/*
Copyright (c) 2016 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.

Author: Leonardo de Moura
*/
#include <string>
#include <utility>
#include "runtime/thread.h"
#include "runtime/flet.h"
#include "util/option_declarations.h"
#include "kernel/find_fn.h"
#include "kernel/for_each_fn.h"
#include "kernel/abstract.h"
#include "kernel/replace_fn.h"
#include "kernel/instantiate.h"
#include "library/scope_pos_info_provider.h"
#include "library/error_msgs.h"
#include "library/trace.h"
#include "library/user_recursors.h"
#include "library/aux_recursors.h"
#include "library/app_builder.h"
#include "library/constants.h"
#include "library/suffixes.h"
#include "library/placeholder.h"
#include "library/explicit.h"
#include "library/string.h"
#include "library/class.h"
#include "library/sorry.h"
#include "library/util.h"
#include "library/annotation.h"
#include "library/pp_options.h"
#include "library/replace_visitor.h"
#include "library/locals.h"
#include "library/private.h"
#include "library/attribute_manager.h"
#include "library/protected.h"
#include "library/message_builder.h"
#include "library/aliases.h"
#include "library/aux_definition.h"
#include "library/check.h"
#include "library/compiler/compiler.h"
#include "library/tactic/kabstract.h"
#include "library/tactic/tactic_state.h"
#include "library/tactic/tactic_evaluator.h"
#include "library/equations_compiler/compiler.h"
#include "library/equations_compiler/util.h"
#include "frontends/lean/builtin_exprs.h"
#include "frontends/lean/brackets.h"
#include "frontends/lean/util.h"
#include "frontends/lean/prenum.h"
#include "frontends/lean/structure_cmd.h"
#include "frontends/lean/structure_instance.h"
#include "frontends/lean/elaborator.h"
#include "frontends/lean/typed_expr.h"
#include "frontends/lean/choice.h"

#ifndef LEAN_DEFAULT_ELABORATOR_COERCIONS
#define LEAN_DEFAULT_ELABORATOR_COERCIONS true
#endif

namespace lean {
static name * g_elab_strategy = nullptr;
static name * g_elaborator_coercions = nullptr;

bool get_elaborator_coercions(options const & opts) {
    return opts.get_bool(*g_elaborator_coercions, LEAN_DEFAULT_ELABORATOR_COERCIONS);
}

struct elaborator_strategy_attribute_data : public attr_data {
    elaborator_strategy m_status;
    elaborator_strategy_attribute_data() {}
    elaborator_strategy_attribute_data(elaborator_strategy status) : m_status(status) {}
    virtual unsigned hash() const override { return static_cast<unsigned>(m_status); }
    void write(serializer & s) const { s << static_cast<char>(m_status); }
    void read(deserializer & d) {
        char c; d >> c;
        m_status = static_cast<elaborator_strategy>(c);
    }
};

bool operator==(elaborator_strategy_attribute_data const & d1, elaborator_strategy_attribute_data const & d2) {
    return d1.m_status == d2.m_status;
}

template class typed_attribute<elaborator_strategy_attribute_data>;
typedef typed_attribute<elaborator_strategy_attribute_data> elaborator_strategy_attribute;

static elaborator_strategy_attribute const & get_elaborator_strategy_attribute() {
    return static_cast<elaborator_strategy_attribute const &>(get_system_attribute(*g_elab_strategy));
}

class elaborator_strategy_proxy_attribute : public proxy_attribute<elaborator_strategy_attribute_data> {
    typedef proxy_attribute<elaborator_strategy_attribute_data> parent;
public:
    elaborator_strategy_proxy_attribute(char const * id, char const * descr, elaborator_strategy status):
        parent(id, descr, status) {}

    virtual typed_attribute<elaborator_strategy_attribute_data> const & get_attribute() const {
        return get_elaborator_strategy_attribute();
    }
};

elaborator_strategy get_elaborator_strategy(environment const & env, name const & n) {
    if (auto data = get_elaborator_strategy_attribute().get(env, n))
        return data->m_status;

    if (is_recursor(env, n) ||
        is_aux_recursor(env, n)) {
        // is_user_defined_recursor(env, n)) {
        return elaborator_strategy::AsEliminator;
    }

    return elaborator_strategy::WithExpectedType;
}

#define trace_elab(CODE) lean_trace("elaborator", scope_trace_env _scope(m_env, m_ctx); CODE)
#define trace_elab_detail(CODE) lean_trace("elaborator_detail", scope_trace_env _scope(m_env, m_ctx); CODE)
#define trace_elab_debug(CODE) lean_trace("elaborator_debug", scope_trace_env _scope(m_env, m_ctx); CODE)
#define trace_elab_numeral(CODE) lean_trace(name({"elaborator", "numeral"}), scope_trace_env _scope(m_env, m_ctx); CODE)
#define trace_elab_instances(CODE) lean_trace(name({"elaborator", "instances"}), scope_trace_env _scope(m_env, m_ctx); CODE)
#define trace_elab_equation(CODE) lean_trace(name({"elaborator", "equation"}), scope_trace_env _scope(m_env, m_ctx); CODE)

elaborator::elaborator(environment const & env, options const & opts, name const & decl_name,
                       metavar_context const & mctx, local_context const & lctx, bool recover_from_errors,
                       bool in_pattern):
    m_env(env), m_opts(opts), m_cache(opts), m_decl_name(decl_name),
    m_ctx(env, mctx, lctx, m_cache, transparency_mode::Semireducible),
    m_recover_from_errors(recover_from_errors),
    m_uses_infom(false /* get_global_info_manager() != nullptr */),
    m_in_pattern(in_pattern) {
    m_coercions = get_elaborator_coercions(opts);
}

elaborator::~elaborator() {
}

auto elaborator::mk_pp_ctx() -> pp_fn {
    return ::lean::mk_pp_ctx(m_ctx.env(), m_opts, m_ctx.mctx(), m_ctx.lctx());
}

formatter elaborator::mk_fmt_ctx() {
    return formatter(m_opts, [=](expr const & e, options const & o) {
        return ::lean::mk_pp_ctx(m_ctx.env(), o, m_ctx.mctx(), m_ctx.lctx())(e);
    });
}

format elaborator::pp_indent(pp_fn const & pp_fn, expr const & e) {
    unsigned i = get_pp_indent(m_opts);
    return nest(i, line() + pp_fn(e));
}

format elaborator::pp_indent(expr const & e) {
    return pp_indent(mk_pp_ctx(), e);
}

format elaborator::pp(expr const & e) {
    auto fn = mk_pp_ctx();
    return fn(e);
}

format elaborator::pp_overload(pp_fn const & pp_fn, expr const & fn) {
    return is_constant(fn) ? format(const_name(fn)) : pp_fn(fn);
}

format elaborator::pp_overloads(pp_fn const & pp_fn, buffer<expr> const & fns) {
    format r("overloads:");
    r += space();
    bool first = true;
    for (expr const & fn : fns) {
        if (first) first = false; else r += format(", ");
        r += pp_overload(pp_fn, fn);
    }
    return paren(r);
}

format elaborator::pp_type_mismatch(expr const & e, expr const & e_type, expr const & expected_type) {
    try {
        expr e_type_type = instantiate_mvars(whnf(infer_type(e_type)));
        expr expected_type_type = instantiate_mvars(whnf(infer_type(expected_type)));
        return ::lean::pp_type_mismatch(mk_fmt_ctx(), e, e_type, expected_type, some_expr(e_type_type), some_expr(expected_type_type));
    } catch (exception &) {
        return ::lean::pp_type_mismatch(mk_fmt_ctx(), e, e_type, expected_type);
    }
}

format elaborator::pp_type_mismatch(expr const & e_type, expr const & expected_type) {
    try {
        expr e_type_type = instantiate_mvars(whnf(infer_type(e_type)));
        expr expected_type_type = instantiate_mvars(whnf(infer_type(expected_type)));
        return ::lean::pp_type_mismatch(mk_fmt_ctx(), e_type, expected_type, some_expr(e_type_type), some_expr(expected_type_type));
    } catch (exception &) {
        return ::lean::pp_type_mismatch(mk_fmt_ctx(), e_type, expected_type);
    }
}

bool elaborator::try_report(std::exception const & ex) {
    return try_report(ex, none_expr());
}

bool elaborator::try_report(std::exception const & ex, optional<expr> const & ref) {
    if (auto elab_ex = dynamic_cast<elaborator_exception const *>(&ex)) {
        if (elab_ex->is_ignored()) return true;
    }
    if (!m_recover_from_errors) return false;

    auto pip = get_pos_info_provider();
    if (!pip) return false;
    auto pos = pip->get_some_pos();
    if (m_last_pos)
        pos = *m_last_pos;
    if (ref)
        if (auto p = get_pos(*ref))
            pos = *p;

    auto tc = std::make_shared<type_context_old>(m_env, m_opts, m_ctx.mctx(), m_ctx.lctx());
    message_builder out(tc, m_env, get_global_ios(), pip->get_file_name(), pos, ERROR);
    out.set_exception(ex);
    out.report();
    m_has_errors = true;
    return true;
}

void elaborator::report_or_throw(elaborator_exception const & ex) {
    if (!try_report(ex))
        throw elaborator_exception(ex);
}

bool elaborator::has_synth_sorry(std::initializer_list<expr> && es) {
    for (auto & e : es) {
        if (has_synthetic_sorry(instantiate_mvars(e))) {
            return true;
        }
    }
    return false;
}

expr elaborator::mk_sorry(optional<expr> const & expected_type, expr const & ref, bool synthetic) {
    auto sorry_type = expected_type ? *expected_type : mk_type_metavar(ref);
    return mk_sorry(sorry_type, synthetic);
}

expr elaborator::recoverable_error(optional<expr> const & expected_type, expr const & ref, elaborator_exception const & ex) {
    report_or_throw(ex);
    return mk_sorry(expected_type, ref);
}


level elaborator::mk_univ_metavar() {
    return m_ctx.mk_univ_metavar_decl();
}

expr elaborator::mk_metavar(expr const & A, expr const & ref) {
    return mk_metavar(name(), A, ref);
}

expr elaborator::mk_metavar(name const & pp_n, expr const & A, expr const &) {
    auto m = m_ctx.mk_metavar_decl(pp_n.is_anonymous() ? pp_n : pp_n.append_before("?"), m_ctx.lctx(), A);
    m_user_mvars.insert(mk_pair(m, user_mvar_info(m_last_pos ? *m_last_pos : pos_info(1, 0))));
    return m;
}

expr elaborator::mk_metavar(optional<expr> const & A, expr const & ref) {
    if (A)
        return mk_metavar(*A, ref);
    else
        return mk_metavar(mk_type_metavar(ref), ref);
}

expr elaborator::mk_type_metavar(expr const & ref) {
    level l = mk_univ_metavar();
    return mk_metavar(mk_sort(l), ref);
}

expr elaborator::mk_instance_core(local_context const & lctx, expr const & C, expr const & ref) {
    scope_traces_as_messages traces_as_messages(get_pos_info_provider(), ref);

    // TODO(gabriel): cache failures so that we do not report errors twice
    optional<expr> inst = m_ctx.mk_class_instance_at(lctx, C);
    if (!inst) {
        metavar_context mctx   = m_ctx.mctx();
        local_context new_lctx = lctx.instantiate_mvars(mctx);
        new_lctx = erase_inaccessible_annotations(new_lctx);
        tactic_state s = ::lean::mk_tactic_state_for(m_env, m_opts, m_decl_name, mctx, new_lctx, C);
        return recoverable_error(some_expr(C), ref, elaborator_exception(
                ref, format("failed to synthesize type class instance for") + line() + s.pp())
            .ignore_if(has_synth_sorry({C})));
    }
    return *inst;
}

expr elaborator::mk_instance_core(expr const & C, expr const & ref) {
    return mk_instance_core(m_ctx.lctx(), C, ref);
}

/* We say a type class (Pi X, (C a_1 ... a_n)), where X may be empty, is
   ready to synthesize if it does not contain metavariables,
   or if the a_i's that contain metavariables are marked as output params. */
bool elaborator::ready_to_synthesize(expr inst_type) {
    if (!has_expr_metavar(inst_type))
        return true;
    while (is_pi(inst_type))
        inst_type = binding_body(inst_type);
    buffer<expr> C_args;
    expr const & C = get_app_args(inst_type, C_args);
    if (!is_constant(C))
        return false;
    expr it = m_ctx.infer(C);
    for (expr const & C_arg : C_args) {
        if (!is_pi(it))
            return false; /* failed */
        expr const & d = binding_domain(it);
        if (has_expr_metavar(C_arg) && !is_class_out_param(d))
            return false;
        it = binding_body(it);
    }
    return true;
}

expr elaborator::mk_instance(expr const & C, expr const & ref) {
    if (!ready_to_synthesize(C)) {
        expr inst = mk_metavar(C, ref);
        m_instances = cons(inst, m_instances);
        return inst;
    } else {
        return mk_instance_core(C, ref);
    }
}

expr elaborator::instantiate_mvars(expr const & e) {
    expr r = m_ctx.instantiate_mvars(e);
    return r;
}

level elaborator::get_level(expr const & A, expr const & ref) {
    expr A_type = whnf(infer_type(A));
    if (is_sort(A_type)) {
        return sort_level(A_type);
    }

    if (is_metavar_app(A_type)) {
        level l = mk_univ_metavar();
        if (try_is_def_eq(A_type, mk_sort(l))) {
            return l;
        }
    }

    auto pp_fn = mk_pp_ctx();
    throw elaborator_exception(ref, pp_type_expected(mk_fmt_ctx(), A, &A_type));
}

level elaborator::replace_univ_placeholder(level const & l) {
    auto fn = [&](level const & l) {
        if (is_placeholder(l))
            return some_level(mk_univ_metavar());
        else
            return none_level();
    };
    return replace(l, fn);
}

/* Here, we say a term is first-order IF all applications are of the form (f ...) where f is a constant. */
static bool is_first_order(expr const & e) {
    return !find(e, [&](expr const & e, unsigned) {
            return is_app(e) && !is_constant(get_app_fn(e));
        });
}

bool elaborator::is_elim_elab_candidate(name const & fn) {
    return get_elaborator_strategy(m_env, fn) == elaborator_strategy::AsEliminator;
}

/* Temporary hack for get_elim_info_for_builtin.
   It doesn't work for drec recursors for inductive predicates.
   TODO(Leo): fix it. */
static bool is_basic_aux_recursor(environment const & env, name const & n) {
    if (!is_aux_recursor(env, n))
        return false;
    return n.get_string() != "drec";
}

/** See comment at elim_info */
auto elaborator::get_elim_info_for_builtin(name const & fn) -> elim_info {
    lean_assert(is_basic_aux_recursor(m_env, fn) || is_recursor(m_env, fn));
    /* Remark: this is not just an optimization. The code at use_elim_elab_core
       only works for dependent elimination. */
    lean_assert(!fn.is_atomic());
    name const & I_name    = fn.get_prefix();

    unsigned nparams;
    unsigned nindices;
    unsigned nminors;

    constant_info I_info = m_env.get(I_name);
    lean_assert(I_info.is_inductive());
    inductive_val I_val  = I_info.to_inductive_val();
    nparams              = I_val.get_nparams();
    nindices             = I_val.get_nindices();
    nminors              = length(I_val.get_cnstrs());

    elim_info r;
    if (fn.get_string() == g_brec_on || fn.get_string() == g_binduction_on) {
        r.m_arity      = nparams + 1 /* motive */ + nindices + 1 /* major */ + 1;
    } else {
        r.m_arity      = nparams + 1 /* motive */ + nindices + 1 /* major */ + nminors;
    }
    r.m_nexplicit  = 1 /* major premise */ + nminors;
    if (nminors == 0) {
        /* The motive is marked as explicit in builtin recursors that do not have
           minor premises */
        r.m_nexplicit++;
    }
    r.m_motive_idx = nparams;
    unsigned major_idx;
    constant_info fn_info = m_env.get(fn);
    if (fn_info.is_recursor()) {
        major_idx = fn_info.to_recursor_val().get_major_idx();
    } else {
        major_idx = nparams + 1 + nindices;
    }
    r.m_idxs = to_list(major_idx);
    return r;
}

/** See comment at elim_info */
auto elaborator::use_elim_elab_core(name const & fn) -> optional<elim_info> {
    if (!is_elim_elab_candidate(fn))
        return optional<elim_info>();
    if (is_basic_aux_recursor(m_env, fn) || is_recursor(m_env, fn)) {
        return optional<elim_info>(get_elim_info_for_builtin(fn));
    }
    type_context_old::tmp_locals locals(m_ctx);
    constant_info d   = m_env.get(fn);
    expr type         = d.get_type();
    while (is_pi(type)) {
        type = instantiate(binding_body(type), locals.push_local_from_binding(type));
    }

    buffer<expr> C_args;
    expr const & C = get_app_args(type, C_args);
    if (!is_local(C) || C_args.empty() || !std::all_of(C_args.begin(), C_args.end(), is_local)) {
        format msg = format("'eliminator' elaboration is not used for '") + format(fn) +
            format("' because resulting type is not of the expected form\n");
        m_elim_failure_info.insert(fn, msg);
        return optional<elim_info>();
    }

    buffer<expr> const & params = locals.as_buffer();
    optional<unsigned> _midx = params.index_of(C);
    if (!_midx)
        return optional<elim_info>();

    unsigned midx = *_midx;
    buffer<unsigned> idxs;
    buffer<bool>     found;
    found.resize(C_args.size(), false);
    unsigned i    = params.size();
    unsigned nexplicit = 0;
    while (i > 0) {
        --i;
        expr const & param = params[i];

        if (!is_explicit(local_info(param))) {
            continue;
        }
        nexplicit++;

        if (optional<unsigned> pos = C_args.index_of(param)) {
            // Parameter is an argument of the resulting type (C ...)
            if (!found[*pos]) {
                // We store it if we could not infer it using the type of other arguments.
                found[*pos] = true;
                idxs.push_back(i);
            }
        }

        expr param_type = m_ctx.infer(param);
        if (!is_first_order(param_type))
            continue;

        bool collected = false;
        for_each(param_type, [&](expr const & e, unsigned) {
                if (is_local(e)) {
                    if (optional<unsigned> pos = C_args.index_of(e)) {
                        if (!found[*pos]) {
                            collected   = true;
                            found[*pos] = true;
                        }
                    }
                }
                return true;
            });
        if (collected)
            idxs.push_back(i);
    }

    for (unsigned i = 0; i < found.size(); i++) {
        if (!found[i]) {
            format msg = format("'eliminator' elaboration is not used for '") + format(fn) +
                format("' because a (reliable) way to synthesize '") + pp(C_args[i]) +
                format("', which occurs in the resulting type, was not found\n");
            m_elim_failure_info.insert(fn, msg);
            return optional<elim_info>();
        }
    }

    std::reverse(idxs.begin(), idxs.end());
    trace_elab_detail(tout() << "'eliminator' elaboration is going to be used for '" << fn << "' applications, "
                      << "the motive is computed using the argument(s):";
                      for (unsigned idx : idxs) tout() << " #" << (idx+1);
                      tout() << "\n";);
    return optional<elim_info>(params.size(), nexplicit, midx, to_list(idxs));
}

/** See comment at elim_info */
auto elaborator::use_elim_elab(name const & fn) -> optional<elim_info> {
    if (auto it = m_elim_cache.find(fn))
        return *it;
    optional<elim_info> r = use_elim_elab_core(fn);
    m_elim_cache.insert(fn, r);
    return r;
}

void elaborator::trace_coercion_failure(expr const & e_type, expr const & type, expr const & ref, char const * error_msg) {
    trace_elab({
            auto pp_fn = mk_pp_ctx();
            format msg("coercion at ");
            msg += format(pos_string_for(ref));
            msg += space() + format("from");
            msg += pp_indent(pp_fn, e_type);
            msg += line() + format("to");
            msg += pp_indent(pp_fn, type);
            msg += line() + format(error_msg);
            tout() << msg << "\n";
        });
}

optional<expr> elaborator::mk_Prop_to_bool_coercion(expr const & e, expr const & ref) {
    expr dec    = mk_app(mk_constant(get_decidable_name()), e);
    expr inst   = mk_instance(dec, ref);
    expr r      = mk_app(mk_constant(get_decidable_decide_name()), e, inst);
    return some_expr(r);
}

static bool is_thunk(expr const & e) {
    return is_app_of(e, get_thunk_name(), 1);
}

optional<expr> elaborator::mk_coercion_core(expr const & e, expr const & e_type, expr const & type, expr const & ref) {
    if (e_type == mk_Prop() && m_ctx.is_def_eq(type, mk_bool())) {
        return mk_Prop_to_bool_coercion(e, ref);
    } else if (is_thunk(type) && m_ctx.is_def_eq(e_type, app_arg(type))) {
        return some_expr(::lean::mk_app(mk_constant(name{"Thunk", "mk"}, const_levels(app_fn(type))),
                                        app_arg(type),
                                        ::lean::mk_lambda("_", mk_constant(get_unit_name()), e)));
    } else if (!has_expr_metavar(e_type) && !has_expr_metavar(type)) {
        expr has_coe_t;
        try {
            has_coe_t = mk_app(m_ctx, get_has_coe_t_name(), e_type, type);
        } catch (app_builder_exception & ex) {
            trace_coercion_failure(e_type, type, ref,
                                   "failed create type class expression 'has_coe_t' "
                                   "('set_option trace.app_builder true' for more information)");
            return none_expr();
        }
        optional<expr> inst;
        try {
            inst = m_ctx.mk_class_instance_at(m_ctx.lctx(), has_coe_t);
        } catch (class_exception &) {
            trace_coercion_failure(e_type, type, ref,
                                   "failed to synthesize class instance for 'has_coe_t' "
                                   "('set_option trace.class_instances true' for more information)");
            return none_expr();
        }
        if (!inst) {
            trace_coercion_failure(e_type, type, ref,
                                   "failed to synthesize 'has_coe_t' type class instance "
                                   "('set_option trace.class_instances true' for more information)");
            return none_expr();
        }
        level u_1 = get_level(e_type, ref);
        level u_2 = get_level(type, ref);
        expr coe_to_lift = mk_app(mk_constant(get_coe_to_lift_name(), {u_1, u_2}), e_type, type, *inst);
        expr coe         = mk_app(mk_constant(get_coe_name(), {u_1, u_2}), e_type, type, coe_to_lift, e);
        return some_expr(coe);
    } else {
        return none_expr();
    }
}

bool elaborator::is_monad(expr const & e) {
    try {
        expr m = mk_app(m_ctx, get_monad_name(), e);
        return static_cast<bool>(m_ctx.mk_class_instance(m));
    } catch (app_builder_exception &) {
        return false;
    } catch (class_exception &) {
        return false;
    }
}

bool elaborator::is_monad_fail(expr const & e) {
    try {
        expr m = mk_app(m_ctx, get_monad_fail_name(), e);
        return static_cast<bool>(m_ctx.mk_class_instance(m));
    } catch (app_builder_exception &) {
        return false;
    } catch (class_exception &) {
        return false;
    }
}

/*
   When lifting monads in do-notation and/or bind, it is very common to have coercion problems such as

          tactic name  ===>  solver ?a

   Coercion resolution cannot be used because (solver ?a) contains meta-variables. Recall that
   coercion resolution is based on type class resolution, and we can only synthesize type class instances
   if the type does not contain meta-variables.

   The coercion problem above is generated in scenarios such as

       do v ← t1,
          ...

   which is notation for

       @bind ?m ?inst ?a ?b t1 (fun v : ?a, ...)

   Now, assume (t1 : tactic name) and the expected type is (solver unit).
   Then, the following meta-variables can be resolved.

        ?m    := solver
        ?b    := unit
        ?inst := solver.monad

   and we get

       @bind solver solver.monad ?a unit t1 (fun v : ?a, ...)

   At this point, we get a type mismatch at t1 because th expected type is (solver ?a) and the given type
   is (tactic name).

   In this method, we consider the following compromise: we assign ?a := name, and then, try to perform
   coercion resolution again.

   Remark: this method also handle the case where the metavariable is at e_type. Example:

          tactic ?a    ===> smt_tactic unit

   TODO(leo): can/should we generalize this approach? */
optional<expr> elaborator::try_monad_coercion(expr const & e, expr e_type, expr type, expr const & ref) {
    if ((has_expr_metavar(e_type) && has_expr_metavar(type))
        || (!has_expr_metavar(e_type) && !has_expr_metavar(type))
        || !is_app(e_type)
        || !is_app(type)
        || has_expr_metavar(app_fn(type))
        || has_expr_metavar(app_fn(e_type))
        || (!is_metavar(app_arg(e_type)) && !is_metavar(app_arg(type)))
        || !is_monad(app_fn(e_type))
        || !is_monad(app_fn(type))) {
        /* Not applicable */
        return none_expr();
    }
    if (!m_ctx.is_def_eq(app_arg(e_type), app_arg(type)))
        return none_expr();
    e_type = instantiate_mvars(e_type);
    type   = instantiate_mvars(type);
    return mk_coercion_core(e, e_type, type, ref);
}

optional<expr> elaborator::mk_coercion(expr const & e, expr e_type, expr type, expr const & ref) {
    if (!m_coercions) return none_expr();
    synthesize_type_class_instances();
    e_type = instantiate_mvars(e_type);
    type   = instantiate_mvars(type);
    if (auto r = try_monad_coercion(e, e_type, type, ref)) {
        return r;
    }
    if (!has_expr_metavar(e_type)) {
        auto whnf_type = whnf(type);
        if (is_pi(whnf_type)) {
            if (auto r = mk_coercion_to_fn(e, e_type, ref)) {
                return r;
            }
        }
        if (is_sort(whnf_type)) {
            if (auto r = mk_coercion_to_sort(e, e_type, ref)) {
                return r;
            }
        }
        if (!has_expr_metavar(type)) {
            return mk_coercion_core(e, e_type, type, ref);
        }
    }
    trace_coercion_failure(e_type, type, ref,
                            "was not considered because types contain metavariables");
    return none_expr();
}

bool elaborator::is_def_eq(expr const & e1, expr const & e2) {
    type_context_old::fo_unif_approx_scope  scope1(m_ctx);
    type_context_old::ctx_unif_approx_scope scope2(m_ctx);
    try {
        return m_ctx.is_def_eq(e1, e2);
    } catch (exception &) {
        return false;
    }
}

/* Check `e1 =?= e2` using all unifier approximation: first-order, context-compression and quasi-patterns. */
bool elaborator::is_def_eq_all_approx(expr const & e1, expr const & e2) {
    type_context_old::approximate_scope scope(m_ctx);
    try {
        return m_ctx.is_def_eq(e1, e2);
    } catch (exception &) {
        return false;
    }
}

bool elaborator::try_is_def_eq(expr const & e1, expr const & e2) {
    snapshot S(*this);
    flet<bool> dont_recover(m_recover_from_errors, false);
    try {
        return is_def_eq(e1, e2);
    } catch (exception &) {
        S.restore(*this);
        throw;
    }
    S.restore(*this);
    return false;
}

optional<expr> elaborator::ensure_has_type(expr const & e, expr const & e_type, expr const & type, expr const & ref) {
    if (is_def_eq(e_type, type))
        return some_expr(e);
    return mk_coercion(e, e_type, type, ref);
}

expr elaborator::enforce_type(expr const & e, expr const & expected_type, char const * header, expr const & ref) {
    expr e_type = infer_type(e);
    if (auto r = ensure_has_type(e, e_type, expected_type, ref)) {
        return *r;
    } else {
        auto exc = elaborator_exception(ref, format(header) + format(", term") +
                pp_type_mismatch(e, e_type, expected_type))
                .ignore_if(has_synth_sorry({e, e_type, expected_type}));
        return recoverable_error(some(expected_type), ref, exc);
    }
}

void elaborator::trace_coercion_fn_sort_failure(bool is_fn, expr const & e_type, expr const & ref, char const * error_msg) {
    trace_elab({
            format msg("coercion at ");
            auto pp_fn = mk_pp_ctx();
            msg += format(pos_string_for(ref));
            msg += space() + format("from");
            msg += pp_indent(pp_fn, e_type);
            if (is_fn)
                msg += line() + format("to function space");
            else
                msg += line() + format("to sort");
            msg += line() + format(error_msg);
            tout() << msg << "\n";
        });
}

optional<expr> elaborator::mk_coercion_to_fn_sort(bool is_fn, expr const & e, expr const & _e_type, expr const & ref) {
    if (!m_coercions) return none_expr();
    expr e_type = instantiate_mvars(_e_type);
    if (!has_expr_metavar(e_type)) {
        try {
            bool mask[3] = { true, false, true };
            expr args[2] = { e_type, e };
            expr new_e = mk_app(m_ctx, is_fn ? get_coe_fn_name() : get_coe_sort_name(), 3, mask, args);
            expr new_e_type = whnf(infer_type(new_e));
            if ((is_fn && is_pi(new_e_type)) || (!is_fn && is_sort(new_e_type))) {
                return some_expr(new_e);
            }
            trace_coercion_fn_sort_failure(is_fn, e_type, ref,
                                           "coercion was successfully generated, but resulting type is not the expected one");
            return none_expr();
        } catch (app_builder_exception & ex) {
            trace_coercion_fn_sort_failure(is_fn, e_type, ref,
                                           "failed create coercion application using type class resolution "
                                           "('set_option trace.app_builder true' and 'set_option trace.class_instances true' for more information)");
            return none_expr();
        }
    } else {
        trace_coercion_fn_sort_failure(is_fn, e_type, ref,
                                       "was not considered because type contain metavariables");
        return none_expr();
    }
}

expr elaborator::ensure_function(expr const & e, expr const & ref) {
    expr e_type = whnf(infer_type(e));
    if (is_pi(e_type)) {
        return e;
    }
    if (auto r = mk_coercion_to_fn(e, e_type, ref)) {
        return *r;
    }
    throw elaborator_exception(ref, pp_function_expected(mk_fmt_ctx(), e, e_type))
            .ignore_if(has_synth_sorry({e, e_type}));
}

expr elaborator::ensure_type(expr const & e, expr const & ref) {
    expr e_type = whnf(infer_type(e));
    if (is_sort(e_type)) {
        return e;
    }

    if (is_metavar_app(e_type) && is_def_eq(e_type, mk_sort(mk_univ_metavar()))) {
        return e;
    }

    if (auto r = mk_coercion_to_sort(e, e_type, ref)) {
        return *r;
    }

    report_or_throw(elaborator_exception(ref, pp_type_expected(mk_fmt_ctx(), e, &e_type)));
    // only create the metavar if can actually recover from the error
    return mk_sorry(some_expr(mk_sort(mk_univ_metavar())), ref);
}

static expr get_ref_for_child(expr const & arg, expr const & ref) {
    if (get_pos_info_provider() && get_pos_info_provider()->get_pos_info(arg)) {
        return arg;
    } else {
        /* using ref because position info for argument is not available */
        return ref;
    }
}

expr elaborator::visit_typed_expr(expr const & e) {
    expr val          = get_typed_expr_expr(e);
    expr ref          = val;
    expr type         = get_typed_expr_type(e);
    expr new_type;
    expr ref_type     = get_ref_for_child(type, e);
    new_type = ensure_type(visit(type, none_expr()), ref_type);
    synthesize_type_class_instances();
    expr new_val      = visit(val, some_expr(new_type));
    expr new_val_type = infer_type(new_val);
    if (auto r = ensure_has_type(new_val, new_val_type, new_type, ref))
        return *r;

    return recoverable_error(some_expr(new_type), ref, elaborator_exception(ref, format("invalid type ascription, term ") +
            pp_type_mismatch(new_val_type, new_type)));
}

level elaborator::dec_level(level const & l, expr const & ref) {
    if (auto d = ::lean::dec_level(l))
        return *d;
    level r = m_ctx.mk_univ_metavar_decl();
    if (!m_ctx.is_def_eq(mk_succ(r), l))
        throw elaborator_exception(ref, "invalid pre-numeral, universe level must be > 0");
    return r;
}

expr elaborator::visit_prenum(expr const & e, optional<expr> const & expected_type) {
    lean_assert(is_prenum(e));
    expr ref = e;
    mpz const & v  = prenum_value(e);
    expr A;
    if (expected_type) {
        A = *expected_type;
        if (is_metavar(*expected_type))
            m_numeral_types = cons(A, m_numeral_types);
    } else {
        A = mk_type_metavar(ref);
        m_numeral_types = cons(A, m_numeral_types);
    }
    level A_lvl = get_level(A, ref);
    levels ls(dec_level(A_lvl, ref));
    if (v.is_neg())
        return recoverable_error(some_expr(A), ref, elaborator_exception(ref, "invalid pre-numeral, it must be a non-negative value"));
    if (v.is_zero()) {
        expr has_zero_A = copy_pos(ref, mk_app(mk_constant(get_has_zero_name(), ls), A));
        expr S          = mk_instance(has_zero_A, ref);
        return copy_pos(ref, mk_app(copy_pos(ref, mk_app(mk_constant(get_has_zero_zero_name(), ls), A)), S));
    } else {
        expr has_one_A = copy_pos(ref, mk_app(mk_constant(get_has_one_name(), ls), A));
        expr S_one     = mk_instance(has_one_A, ref);
        expr one       = copy_pos(ref, mk_app(copy_pos(ref, mk_app(mk_constant(get_has_one_one_name(), ls), A)), S_one));
        if (v == 1) {
            return one;
        } else {
            expr has_add_A = copy_pos(ref, mk_app(mk_constant(get_has_add_name(), ls), A));
            expr S_add     = mk_instance(has_add_A, ref);
            std::function<expr(mpz const & v)> convert = [&](mpz const & v) {
                lean_assert(v > 0);
                if (v == 1)
                    return one;
                else if (v % mpz(2) == 0) {
                    expr r = convert(v / 2);
                    return copy_pos(ref, mk_app(copy_pos(ref, mk_app(copy_pos(ref, mk_app(mk_constant(get_bit0_name(), ls), A)), S_add)), r));
                } else {
                    expr r = convert(v / 2);
                    return copy_pos(ref, mk_app(copy_pos(ref, mk_app(copy_pos(ref, mk_app(copy_pos(ref, mk_app(mk_constant(get_bit1_name(), ls), A)), S_one)), S_add)), r));
                }
            };
            return convert(v);
        }
    }
}

expr elaborator::visit_sort(expr const & e) {
    level new_l = replace_univ_placeholder(sort_level(e));
    expr r = update_sort(e, new_l);
    if (has_placeholder(sort_level(e)))
        m_to_check_sorts.emplace_back(e, r);
    return r;
}

expr elaborator::visit_const_core(expr const & e) {
    constant_info d = m_env.get(const_name(e));
    buffer<level> ls;
    for (level const & l : const_levels(e)) {
        level new_l = replace_univ_placeholder(l);
        ls.push_back(new_l);
    }
    unsigned num_lparams = d.get_num_lparams();
    if (num_lparams < ls.size()) {
        format msg("incorrect number of universe levels parameters for '");
        msg += format(const_name(e)) + format("', #") + format(num_lparams);
        msg += format(" expected, #") + format(ls.size()) + format("provided");
        return recoverable_error({}, e, elaborator_exception(e, msg));
    }
    // "fill" with meta universe parameters
    for (unsigned i = ls.size(); i < num_lparams; i++)
        ls.push_back(mk_univ_metavar());
    lean_assert(num_lparams == ls.size());
    return update_constant(e, levels(ls));
}

expr elaborator::visit_function(expr const & fn, bool has_args, optional<expr> const & expected_type, expr const & ref) {
    expr ufn = unwrap_pos(fn);
    if (is_placeholder(ufn)) {
        throw elaborator_exception(ref, "placeholders '_' cannot be used where a function is expected");
    }
    if (is_field_notation(ufn))
        throw elaborator_exception(ref, "invalid occurrence of field notation");
    expr r;
    switch (ufn.kind()) {
    case expr_kind::BVar:
    case expr_kind::Pi:
    case expr_kind::MVar:
    case expr_kind::Sort:
    case expr_kind::Lit:
        throw elaborator_exception(ref, "invalid application, function expected");
    // The expr_kind::App case can only happen when nary notation is used
    case expr_kind::App:       r = visit(ufn, expected_type); break;
    case expr_kind::FVar:      r = ufn; break;
    case expr_kind::Const:     r = visit_const_core(ufn); break;
    case expr_kind::MData:     r = visit_mdata(ufn, expected_type, true); break;
    case expr_kind::Lambda:    r = visit_lambda(ufn, expected_type); break;
    case expr_kind::Let:       r = visit_let(ufn, expected_type); break;
    case expr_kind::Proj:      throw elaborator_exception(ref, "unexpected occurrence of proj constructor");
    }
    if (has_args)
        r = ensure_function(r, ref);
    return r;
}

void elaborator::validate_overloads(buffer<expr> const & fns, expr const & ref) {
    for (expr const & fn_i : fns) {
        if (is_constant(fn_i) && use_elim_elab(const_name(fn_i))) {
            auto pp_fn = mk_pp_ctx();
            format msg("invalid overloaded application, "
                       "elaborator has special support for '");
            msg += pp_fn(fn_i);
            msg += format("' (it is handled as an \"eliminator\"), "
                          "but this kind of constant cannot be overloaded "
                          "(solution: use fully qualified names) ");
            msg += pp_overloads(pp_fn, fns);
            throw elaborator_exception(ref, msg);
        }
    }
}

void elaborator::throw_app_type_mismatch_error(expr const & t, expr const & arg, expr const & arg_type,
                                               expr const & expected_type, expr const & ref) {
    format msg   = format("type mismatch at application");
    msg += pp_indent(mk_pp_ctx(), t);
    msg += line() + format("term") + pp_type_mismatch(arg, arg_type, expected_type);
    throw elaborator_exception(ref, msg).
            ignore_if(has_synth_sorry({arg, arg_type, expected_type}));
}

format elaborator::mk_app_arg_mismatch_error(expr const & t, expr const & arg, expr const & expected_arg) {
    auto pp_data = pp_until_different(mk_fmt_ctx(), arg, expected_arg);
    auto fmt     = std::get<0>(pp_data);
    format msg = format("unexpected argument at application");
    msg += pp_indent_expr(fmt, t);
    msg += line() + format("given argument");
    msg += std::get<1>(pp_data);
    msg += line() + format("expected argument");
    msg += std::get<2>(pp_data);
    return msg;
}

expr elaborator::visit_elim_app(expr const & fn, elim_info const & info, buffer<expr> const & args,
                                optional<expr> const & _expected_type, expr const & ref) {
    trace_elab_detail(tout() << "recursor/eliminator application at " << pos_string_for(ref) << "\n";);
    lean_assert(info.m_nexplicit <= args.size());
    if (!_expected_type) {
        throw elaborator_exception(ref, format("invalid '") + format(const_name(fn)) + format ("' application, ") +
                                   format("elaborator has special support for this kind of application ") +
                                   format("(it is handled as an \"eliminator\"), ") +
                                   format("but the expected type must be known"));
    }
    synthesize_type_class_instances();
    expr expected_type = instantiate_mvars(*_expected_type);
    if (has_expr_metavar(expected_type)) {
        auto pp_fn = mk_pp_ctx();
        throw elaborator_exception(ref, format("invalid '") + format(const_name(fn)) + format ("' application, ") +
                                   format("elaborator has special support for this kind of application ") +
                                   format("(it is handled as an \"eliminator\"), ") +
                                   format("but expected type must not contain metavariables") +
                                   pp_indent(pp_fn, expected_type));
    }

    trace_elab_debug(
        tout() << "eliminator elaboration for '" << fn << "'\n"
        << "  arity:     " << info.m_arity << "\n"
        << "  nexplicit: " << info.m_nexplicit << "\n"
        << "  motive:    #" << (info.m_motive_idx+1) << "\n"
        << "  \"major\":  ";
        for (unsigned idx : info.m_idxs)
            tout() << " #" << (idx+1);
        tout() << "\n";);

    expr fn_type = try_to_pi(infer_type(fn));
    buffer<expr> new_args;

    /* In the first pass we only process the arguments at info.m_idxs */
    expr type = fn_type;
    unsigned i = 0;
    unsigned j = 0;
    list<unsigned> main_idxs  = info.m_idxs;
    buffer<optional<expr>> postponed_args; // mark arguments that must be elaborated in the second pass.
    {
        while (is_pi(type)) {
            expr const & d = binding_domain(type);
            binder_info bi = binding_info(type);
            expr new_arg;
            optional<expr> postponed;
            if (std::find(main_idxs.begin(), main_idxs.end(), i) != main_idxs.end()) {
                {
                    new_arg = visit(args[j], some_expr(d));
                    synthesize();
                    new_arg = instantiate_mvars(new_arg);
                }
                j++;
                if (has_expr_metavar(new_arg)) {
                    auto pp_fn = mk_pp_ctx();
                    throw elaborator_exception(ref, format("invalid '") + format(const_name(fn)) +
                                               format ("' application, ") +
                                               format("elaborator has special support for this kind of application ") +
                                               format("(it is handled as an \"eliminator\"), ") +
                                               format("but term") + pp_indent(pp_fn, new_arg) +
                                               line() + format("must not contain metavariables because") +
                                               format(" it is used to compute the motive"));
                }
                expr new_arg_type = infer_type(new_arg);
                if (!is_def_eq(new_arg_type, d)) {
                    new_args.push_back(new_arg);
                    throw_app_type_mismatch_error(mk_app(fn, new_args), new_arg, new_arg_type, d, ref);
                }
            } else if (is_explicit(bi)) {
                expr arg_ref = args[j];
                new_arg      = mk_metavar(d, arg_ref);
                postponed    = args[j];
                j++;
            } else if (is_inst_implicit(bi)) {
                new_arg = mk_instance(d, ref);
            } else {
                new_arg = mk_metavar(d, ref);
            }
            new_args.push_back(new_arg);
            postponed_args.push_back(postponed);
            type = try_to_pi(instantiate(binding_body(type), new_arg));
            i++;
        }

        lean_assert(new_args.size() == info.m_arity);

        /* Process extra arguments */
        for (; j < args.size(); j++) {
            new_args.push_back(visit(args[j], none_expr()));
        }
        synthesize();
    }

    /* Compute expected_type for the recursor application without extra arguments */
    buffer<expr> extra_args;
    i = new_args.size();
    while (i > info.m_arity) {
        --i;
        expr new_arg      = instantiate_mvars(new_args[i]);
        expr new_arg_type = instantiate_mvars(infer_type(new_arg));
        expected_type     = ::lean::mk_pi("_a", new_arg_type, kabstract(m_ctx, expected_type, new_arg));
        extra_args.push_back(new_arg);
    }
    new_args.shrink(i);
    std::reverse(extra_args.begin(), extra_args.end());

    // Compute motive */
    trace_elab_debug(tout() << "compute motive by using keyed-abstraction:\n  " <<
                     instantiate_mvars(type) << "\nwith\n  " <<
                     expected_type << "\n";);
    expr motive = expected_type;
    buffer<expr> keys;
    get_app_args(type, keys);
    i = keys.size();
    while (i > 0) {
        --i;
        expr k = instantiate_mvars(keys[i]);
        expr k_type    = infer_type(k);
        motive = ::lean::mk_lambda("_x", k_type, kabstract(m_ctx, motive, k));
    }
    trace_elab_debug(tout() << "motive:\n  " << instantiate_mvars(motive) << "\n";);

    expr motive_arg = new_args[info.m_motive_idx];
    trace_elab_debug(tout() << "motive_arg:\n  " << motive_arg << "\n";);
    if (!is_def_eq_all_approx(motive_arg, motive)) {
        throw elaborator_exception(ref, "\"eliminator\" elaborator failed to compute the motive");
    }

    try {
        /* Elaborate postponed arguments */
        for (unsigned i = 0; i < new_args.size(); i++) {
            if (optional<expr> arg = postponed_args[i]) {
                lean_assert(is_metavar(new_args[i]));
                expr new_arg_type = instantiate_mvars(infer_type(new_args[i]));
                expr new_arg      = visit(*arg, some_expr(new_arg_type));
                if (!is_def_eq(new_args[i], new_arg)) {
                    throw elaborator_exception(ref, format("\"eliminator\" elaborator type mismatch, term") +
                                               pp_type_mismatch(new_arg, infer_type(new_arg), new_arg_type));
                } else {
                    new_args[i] = new_arg;
                }
            }
        }

        expr r = instantiate_mvars(mk_app(mk_app(fn, new_args), extra_args));
        trace_elab_debug(tout() << "elaborated recursor:\n  " << r << "\n";);
        return r;
    } catch (elaborator_exception & ex) {
        // TODO(gabriel): the additional information is not added in error-recovery mode
        throw nested_elaborator_exception(ref, std::current_exception(),
                                          format("the inferred motive for the eliminator-like application is") +
                                          pp_indent(motive));
    }
}

struct elaborator::first_pass_info {
    buffer<expr> args_mvars;
    buffer<expr> args_expected_types;
    buffer<expr> new_args;
    /* new_args_size[i] contains size of new_args after args_mvars[i] was pushed.
       We need this information for producing error messages. */
    buffer<unsigned> new_args_size;
    buffer<expr>     new_instances;
    /* new_instances_size[i] contains the size of new_instances before (and after) args_mvars[i]
       was pushed. */
    buffer<unsigned> new_instances_size;
    /* Store arguments that need to be abstracted when we apply eta expansion for function applications
       containing optional and auto parameters. */
    buffer<expr>     eta_args;
};

expr elaborator::mk_auto_param(expr const &, expr const &, expr const &) {
    throw exception("auto_param has been disabled (we removed the tactic framework");
}


optional<expr> elaborator::process_optional_and_auto_params(expr type, expr const & ref, buffer<expr> & eta_args, buffer<expr> & new_args) {
    unsigned sz1 = eta_args.size();
    unsigned sz2 = new_args.size();
    optional<expr> result_type;
    while (true) {
        expr it = whnf(type);
        if (!is_pi(it))
            break;
        type = it;
        expr const & d = binding_domain(type);
        expr new_arg;
        bool found = false;
        if (auto def_value = is_optional_param(d)) {
            found   = true;
            new_arg = *def_value;
        } else if (auto p = is_auto_param(d)) {
            found   = true;
            new_arg = mk_auto_param(p->second, p->first, ref);
        } else {
            new_arg = mk_local(mk_fresh_name(), binding_name(type), d, binding_info(type));
            eta_args.push_back(new_arg);
        }
        new_args.push_back(new_arg);
        type = instantiate(binding_body(type), new_arg);
        if (found) {
            result_type = type;
            sz1 = eta_args.size();
            sz2 = new_args.size();
        }
    }
    eta_args.shrink(sz1);
    new_args.shrink(sz2);
    if (result_type)
        return some_expr(Pi(eta_args, *result_type));
    else
        return result_type;
}

expr elaborator::post_process_implicit_arg(expr const & arg, expr const & /* ref */) {
    return arg;
}

/* Check if fn args resulting type matches the expected type, and fill
   first_pass_info & info with information collected in this first pass.
   Return true iff the types match.

   Remark: the arguments \c args are *not* visited in this first pass.
   They are only used in this method to provide location information. */
void elaborator::first_pass(expr const & fn, buffer<expr> const & args,
                            expr const & expected_type, expr const & ref,
                            first_pass_info & info) {
    expr fn_type          = infer_type(fn);
    expr type_before_whnf = fn_type;
    expr type             = whnf(fn_type);
    unsigned i   = 0;
    /* First pass: compute type for an fn-application, and unify it with expected_type.
       We don't visit expelicit arguments at this point. */

    /* Outer loop is used to make sure we consume implicit arguments occurring after auto/option params. */
    while (true) {
        while (is_pi(type)) {
            binder_info bi = binding_info(type);
            expr const & d = binding_domain(type);
            if (is_strict_implicit(bi) && i == args.size())
                break;
            expr new_arg;
            if (!is_explicit(bi)) {
                // implicit argument
                new_arg = mk_metavar(binding_name(type), d, ref);
                if (is_inst_implicit(bi))
                    info.new_instances.push_back(new_arg);
                new_arg = post_process_implicit_arg(new_arg, ref);
            } else if (i < args.size()) {
                // explicit argument
                expr const & arg_ref = args[i];
                info.args_expected_types.push_back(d);
                if (is_as_is(args[i])) {
                    /* We check the type of as-is arguments eagerly, and
                       we don't create a metavariable for them since they
                       have already been elaborated.

                       This is important when we are elaborating terms such as

                       l.map (λ ⟨a, b⟩, a + b)

                       where (l : list (nat × nat))

                       We elaborate l when we process l.map, and convert it into

                       list.map (λ ⟨a, b⟩, a + b) (as-is l)

                       By checking the type of l here, we make sure that the
                       domain of the function (λ ⟨a, b⟩, a + b) is known when
                       we elaborate it.

                       Note that if the user types

                       list.map (λ ⟨a, b⟩, a + b) l
                                 --^ fails here

                       It will fail because we elaborate from left to right, and
                       we don't have the type of l.

                       See discussion at #1403
                    */
                    new_arg = get_as_is_arg(args[i]);
                    expr arg_expected_type;
                    arg_expected_type = d;
                    expr new_arg_type = infer_type(new_arg);
                    optional<expr> new_new_arg = ensure_has_type(new_arg, new_arg_type, arg_expected_type, arg_ref);
                    if (!new_new_arg) {
                        buffer<expr> tmp_args;
                        tmp_args.append(info.new_args);
                        tmp_args.push_back(new_arg);
                        throw_app_type_mismatch_error(mk_app(fn, tmp_args), new_arg, new_arg_type, arg_expected_type, ref);
                    }
                    new_arg = *new_new_arg;
                } else {
                    new_arg = mk_metavar(d, arg_ref);
                }
                i++;
                lean_assert(!get_pos(new_arg));
                info.args_mvars.push_back(new_arg);
                info.new_args_size.push_back(info.new_args.size());
                info.new_instances_size.push_back(info.new_instances.size());
            } else {
                break;
            }
            info.new_args.push_back(new_arg);
            /* See comment above at visit_base_app_core */
            type_before_whnf = instantiate(binding_body(type), new_arg);
            type             = whnf(type_before_whnf);
        }
        type = type_before_whnf;
        if (optional<expr> new_type = process_optional_and_auto_params(type, ref, info.eta_args, info.new_args)) {
            type = *new_type;
            /* If next argument is implicit, then keep consuming */
            type_before_whnf = *new_type;
            type             = whnf(type_before_whnf);
            if (!is_pi(type) || is_explicit(binding_info(type))) {
                type = type_before_whnf;
                break;
            }
        } else {
            break;
        }
    }

    if (i != args.size()) {
        /* failed to consume all explicit arguments, use base elaboration for applications */
        throw elaborator_exception(ref, "too many arguments");
    }
    lean_assert(args.size() == info.args_expected_types.size());
    lean_assert(args.size() == info.args_mvars.size());
    lean_assert(args.size() == info.new_args_size.size());
    lean_assert(args.size() == info.new_instances_size.size());

    if (!is_def_eq(expected_type, type)) {
        expr e = mk_app(fn, info.new_args);
        throw elaborator_exception(ref, format("type mismatch, term") + pp_type_mismatch(e, type, expected_type))
            .ignore_if(has_synth_sorry({type, expected_type, e}));
    }
}

std::tuple<expr, expr, optional<expr>> elaborator::elaborate_arg(expr const & arg, expr const & expected_type, expr const & ref) {
    expr aux_expected_type = expected_type;
    expr new_arg = visit(arg, some_expr(aux_expected_type));
    expr new_arg_type = infer_type(new_arg);
    return std::make_tuple(new_arg, new_arg_type, ensure_has_type(new_arg, new_arg_type, expected_type, ref));
}

/* Using the information colllected in the first-pass, visit the arguments args.
   And then, create resulting application */
expr elaborator::second_pass(expr const & fn, buffer<expr> const & args,
                             expr const & ref, first_pass_info & info) {
    unsigned j = 0; /* for traversing new_instances */
    /* Second pass: visit explicit arguments using the information
       we gained about their expected types */
    for (unsigned i = 0; i < args.size(); i++) {
        /* Process type class instances upto args[i] */
        for (; j < info.new_instances_size[i]; j++) {
            expr const & mvar = info.new_instances[j];
            if (!try_synthesize_type_class_instance(mvar))
                m_instances = cons(mvar, m_instances);
        }
        expr ref_arg       = get_ref_for_child(args[i], ref);
        expr expected_type = info.args_expected_types[i];
        info.new_args[info.new_args_size[i]] = recover_expr_from_exception(some_expr(expected_type), ref_arg, [&] {
            if (is_metavar(info.args_mvars[i])) {
                expr new_arg; expr new_arg_type; optional<expr> new_new_arg;
                std::tie(new_arg, new_arg_type, new_new_arg) = elaborate_arg(args[i], expected_type, ref_arg);
                if (!new_new_arg) {
                    buffer<expr> tmp_args;
                    tmp_args.append(info.new_args_size[i], info.new_args.data());
                    tmp_args.push_back(new_arg);
                    throw_app_type_mismatch_error(mk_app(fn, tmp_args), new_arg, new_arg_type,
                                                  info.args_expected_types[i], ref);
                }
                if (!is_def_eq(info.args_mvars[i], *new_new_arg)) {
                    buffer<expr> tmp_args;
                    tmp_args.append(info.new_args_size[i], info.new_args.data());
                    tmp_args.push_back(new_arg);
                    format msg = mk_app_arg_mismatch_error(mk_app(fn, tmp_args),
                                                           new_arg, info.args_mvars[i]);
                    report_or_throw(elaborator_exception(ref, msg).
                        ignore_if(has_synth_sorry({new_arg, instantiate_mvars(info.args_mvars[i])})));
                }
                return *new_new_arg;
            } else {
                return info.args_mvars[i];
            }
        });
    }
    for (; j < info.new_instances.size(); j++) {
        expr const & mvar = info.new_instances[j];
        if (!try_synthesize_type_class_instance(mvar))
            m_instances = cons(mvar, m_instances);
    }
    expr e = Fun(info.eta_args, mk_app(fn, info.new_args.size(), info.new_args.data()));
    for (auto const & new_arg : info.new_args)
        if (m_user_mvars.count(new_arg))
            m_user_mvars[new_arg].m_context = some_expr(e);
    return e;
}

bool elaborator::is_with_expected_candidate(expr const & fn) {
    /* When processing expressions such as (l^.for f), we first resolve the field notation obtaining
           (list.for l)
       Then, when processing ((list.for l) f), we still want to use the expected type.
       So, we should retrieve the constant list.for using get_app_fn.
    */
    expr _fn = get_app_fn(fn);
    if (!is_constant(_fn)) return false;
    return get_elaborator_strategy(m_env, const_name(_fn)) == elaborator_strategy::WithExpectedType;
}

expr elaborator::visit_base_app_simple(expr const & _fn, arg_mask amask, buffer<expr> const & args,
                                       bool args_already_visited, optional<expr> const & expected_type, expr const & ref) {
    expr fn      = _fn;
    expr fn_type = infer_type(fn);
    unsigned i = 0;
    buffer<expr> new_args;

    /* We manually track the type before whnf. We do that because after the loop
       we check whether the application type is definitionally equal to expected_type.
       Suppose, the result type is (tactic expr) and the expected type is (?M1 ?M2).
       The unification problem can be solved using first-order unification, but if we
       unfold (tactic expr), we get (tactic_state -> base_tactic_result ...) which cannot.

       The statement
         expr type = try_to_pi(fn_type);
       also does not work because (tactic expr) unfolds into a type. */
    expr type_before_whnf = fn_type;
    expr type             = whnf(fn_type);
    buffer<expr> eta_args;
    /* Outer loop is used to make sure we consume implicit arguments occurring after auto/option params. */
    while (true) {
        while (true) {
            if (is_pi(type)) {
                binder_info bi = binding_info(type);
                expr const & d = binding_domain(type);
                expr new_arg;
                if (amask == arg_mask::Default && is_strict_implicit(bi) && i == args.size())
                    break;
                if ((amask == arg_mask::Default && !is_explicit(bi)) ||
                    (amask == arg_mask::InstHoExplicit && !is_explicit(bi) && !is_inst_implicit(bi) && !is_pi(d))) {
                    // implicit argument
                    if (is_inst_implicit(bi))
                        new_arg = mk_instance(d, ref);
                    else
                        new_arg = mk_metavar(binding_name(type), d, ref);
                    new_arg = post_process_implicit_arg(new_arg, ref);
                } else if (i < args.size()) {
                    expr expected_type = d;
                    // explicit argument
                    expr ref_arg = get_ref_for_child(args[i], ref);
                    if (args_already_visited) {
                        new_arg = args[i];
                    } else if (is_inst_implicit(bi) && is_placeholder(args[i])) {
                        lean_assert(amask != arg_mask::Default);
                    /* If '@' or '@@' have been used, and the argument is '_', then
                       we use type class resolution. */
                        new_arg = mk_instance(d, ref);
                    } else {
                        new_arg = visit(args[i], some_expr(expected_type));
                    }
                    expr new_arg_type = infer_type(new_arg);
                    if (optional<expr> new_new_arg = ensure_has_type(new_arg, new_arg_type, d, ref_arg)) {
                        new_arg = *new_new_arg;
                    } else {
                        new_args.push_back(new_arg);
                        throw_app_type_mismatch_error(mk_app(fn, new_args.size(), new_args.data()),
                                                      new_arg, new_arg_type, d, ref);
                    }
                    i++;
                } else {
                    break;
                }
                new_args.push_back(new_arg);
                /* See comment above at first type_before_whnf assignment */
                type_before_whnf = instantiate(binding_body(type), new_arg);
                type             = whnf(type_before_whnf);
           } else if (i < args.size()) {
                bool progress = false;
                /* If type is of the form `?m ...`, we may try to synthesize pending type class problems,
                   and check whether `?m` has been instantiated or not.

                   We need this trick to handle `monad_run.run`. This function has type
                   ```
                   run : Π {out m : Type u_1 → Type u_2} [c : monad_run out m] {α : Type u_1}, m α → out α
                   ```
                   The resultant type `out α` depends on resolving the type class resolution problem `monad_run out m`
                   since `out` is marked as an `out_param`.

                   Without this approach, we would have to write
                   ```
                   @[reducible] def type_checker_m := except_t format (reader_t (environment × list result) (state_t context id))

                   def type_checker_m.run {α} (a : type_checker_m α) (env : environment) (r : list result) : except format α :=
                   (run a (env, r) mk_context).1
                   ```
                   as
                   ```
                   @[reducible] def type_checker_m := except_t format (reader_t (environment × list result) (state_t context id))

                   def type_checker_m.run {α} (a : type_checker_m α) (env : environment) (r : list result) : except format α :=
                   let tmp := run a in
                   (tmp (env, r) mk_context).1
                   ```
                   or completely avoid `run`. */
                if (is_metavar_app(type)) {
                    synthesize_type_class_instances();
                    expr new_type = instantiate_mvars(type);
                    if (new_type != type) {
                        type = new_type;
                        progress = true;
                    }
                }
                if (!progress) {
                    expr new_fn = mk_app(fn, new_args.size(), new_args.data());
                    new_args.clear();
                    fn = ensure_function(new_fn, ref);
                    type_before_whnf = infer_type(fn);
                    type = whnf(type_before_whnf);
                }
            } else {
                lean_assert(i == args.size());
                break;
            }
        }
        type = instantiate_mvars(type_before_whnf);

        /* We ignore optional/auto params when `@` is used */
        if (amask != arg_mask::Default)
            break;

        if (optional<expr> new_type = process_optional_and_auto_params(type, ref, eta_args, new_args)) {
            /* If next argument is implicit, then keep consuming */
            type_before_whnf = *new_type;
            type             = whnf(type_before_whnf);
            if (!is_pi(type) || is_explicit(binding_info(type))) {
                type = type_before_whnf;
                break;
            }
        } else {
            break;
        }
    }

    expr r = Fun(eta_args, mk_app(fn, new_args.size(), new_args.data()));
    for (auto const & new_arg : new_args)
        if (m_user_mvars.count(new_arg))
            m_user_mvars[new_arg].m_context = some_expr(r);
    if (expected_type) {
        if (auto new_r = ensure_has_type(r, type, *expected_type, ref)) {
            return *new_r;
        } else {
            /* We do not generate the error here because we can produce a better one from
               the caller (i.e., place the set the expected_type) */
        }
    }
    return r;
}

expr elaborator::visit_base_app_core(expr const & fn, arg_mask amask, buffer<expr> const & args,
                                     bool args_already_visited, optional<expr> const & expected_type, expr const & ref) {
    /** Check if this application is a candidate for propagating the expected type to arguments */
    if (args_already_visited || /* if the arguments have already been visited, then there is no point. */
        amask != arg_mask::Default || /* we do not propagate expected type info when @ and @@ are used */
        !is_with_expected_candidate(fn) ||
        !expected_type) {
        return visit_base_app_simple(fn, amask, args, args_already_visited, expected_type, ref);
    }

    snapshot C(*this);
    first_pass_info info;
    try {
        /* In the first pass, we skip universe contraints such as

               (max 1 ?m) =?= ?m
               (max ?m1 1) =?= (max 1 ?m2)

           The first one cannot be solved by type_context_old, and an approximate
           solution is used for the second one. In the second pass, we will have
           additional information propagated from the expected_type, and these
           constraints may become trivial.

           The following definition illustrates the issue:

              meta def foo (ex_lst : list name) (e : expr) : list name :=
              expr.fold e [] (λ _ _ l, l)

           expr.fold has type

              meta constant expr.fold {α : Type} : expr → α → (expr → nat → α → α) → α

           So, when processing [], we have to unify ?α with (list ?m), where

                     ?α      : Type 1
                     list ?m : Type (max 1 ?u)

           Thus, we also have to solve

                   1 =?= (max 1 ?u)

           which, as described above, cannot be solved by type_context_old.
           However, this constraint becomes trivial after we propagate the expected type
           (list name), and we have to solve (list name) =?= (list ?m)
        */
        type_context_old::full_postponed_scope scope(m_ctx, false);
        flet<bool> dont_recover(m_recover_from_errors, false);
        first_pass(fn, args, *expected_type, ref, info);
    } catch (elaborator_exception & ex1) {
        C.restore(*this);
        try {
            return visit_base_app_simple(fn, amask, args, args_already_visited, expected_type, ref);
        } catch (elaborator_exception & ex2) {
            throw nested_elaborator_exception(ref, std::current_exception(),
                                              format("switched to simple application elaboration procedure because "
                                                     "failed to use expected type to elaborate it, "
                                                     "error message") + nest(get_pp_indent(m_opts), line() + ex1.pp()));
        }
    }
    return second_pass(fn, args, ref, info);
}

expr elaborator::visit_base_app(expr const & fn, arg_mask amask, buffer<expr> const & args,
                                optional<expr> const & expected_type, expr const & ref) {
    return visit_base_app_core(fn, amask, args, false, expected_type, ref);
}

expr elaborator::visit_overload_candidate(expr const & fn, buffer<expr> const & args,
                                          optional<expr> const & expected_type, expr const & ref) {
    return visit_base_app_core(fn, arg_mask::Default, args, true, expected_type, ref);
}

format elaborator::mk_no_overload_applicable_msg(buffer<expr> const & fns, buffer<elaborator_exception> const & error_msgs) {
    format r("none of the overloads are applicable");
    lean_assert(error_msgs.size() == fns.size());
    for (unsigned i = 0; i < fns.size(); i++) {
        if (i > 0) r += line();
        auto pp_fn = mk_pp_ctx();
        r += line() + format("error for") + space() + pp_overload(pp_fn, fns[i]);
        r += line() + error_msgs[i].pp();
    }
    return r;
}

expr elaborator::visit_overloaded_app_core(buffer<expr> const & fns, buffer<expr> const & args,
                                           optional<expr> const & expected_type, expr const & ref) {
    buffer<expr> new_args;
    for (expr const & arg : args) {
        new_args.push_back(visit(arg, none_expr()));
    }

    snapshot S(*this);

    buffer<pair<expr, snapshot>> candidates;
    buffer<elaborator_exception> error_msgs;
    for (expr const & fn : fns) {
        try {
            flet<bool> dont_recover(m_recover_from_errors, false);
            // Restore state
            S.restore(*this);
            bool has_args = !args.empty();
            expr new_fn   = visit_function(fn, has_args, has_args ? none_expr() : expected_type, ref);
            expr c        = visit_overload_candidate(new_fn, new_args, expected_type, ref);
            synthesize_type_class_instances();

            if (expected_type) {
                expr c_type = infer_type(c);
                if (ensure_has_type(c, c_type, *expected_type, ref)) {
                    candidates.emplace_back(c, snapshot(*this));
                } else {
                    throw elaborator_exception(ref, format("invalid overload, term") +
                            pp_type_mismatch(c, c_type, *expected_type));
                }
            } else {
                candidates.emplace_back(c, snapshot(*this));
            }
        } catch (elaborator_exception & ex) {
            error_msgs.push_back(ex);
        } catch (exception & ex) {
            error_msgs.push_back(elaborator_exception(ref, format(ex.what())));
        }
    }
    lean_assert(candidates.size() + error_msgs.size() == fns.size());

    if (candidates.empty()) {
        S.restore(*this);

        throw elaborator_exception(ref, mk_no_overload_applicable_msg(fns, error_msgs));
    } else if (candidates.size() > 1) {
        S.restore(*this);

        options new_opts = m_opts.update_if_undef(get_pp_full_names_name(), true);
        flet<options> set_opts(m_opts, new_opts);
        auto pp_fn = mk_pp_ctx();
        format r("ambiguous overload, possible interpretations");
        for (auto const & c : candidates) {
            r += pp_indent(pp_fn, c.first);
        }
        throw elaborator_exception(ref, r);
    } else {
        // Restore successful state
        candidates[0].second.restore(*this);
        return candidates[0].first;
    }
}

expr elaborator::visit_overloaded_app_with_expected(buffer<expr> const & fns, buffer<expr> const & args,
                                                    expr const & expected_type, expr const & ref) {
    snapshot S(*this);
    buffer<std::tuple<expr, snapshot, first_pass_info>> candidates;
    buffer<elaborator_exception> error_msgs;
    for (expr const & fn : fns) {
        try {
            flet<bool> dont_recover(m_recover_from_errors, false);
            // Restore state
            S.restore(*this);
            bool has_args = !args.empty();
            expr new_fn   = visit_function(fn, has_args, has_args ? none_expr() : some(expected_type), ref);
            first_pass_info info;
            first_pass(new_fn, args, expected_type, ref, info);
            candidates.emplace_back(new_fn, snapshot(*this), info);
        } catch (elaborator_exception & ex) {
            error_msgs.push_back(ex);
        } catch (exception & ex) {
            error_msgs.push_back(elaborator_exception(ref, format(ex.what())));
        }
    }

    if (candidates.empty()) {
        try {
            /* Failed to disambiguate using expected type, switch to basic */
            S.restore(*this);
            return visit_overloaded_app_core(fns, args, some_expr(expected_type), ref);
        } catch (elaborator_exception & ex) {
            auto pp_fn = mk_pp_ctx();
            format msg = format("switched to basic overload resolution where arguments are elaborated without "
                                "any information about the expected type, because failed to elaborate all candidates "
                                "using the expected type");
            msg += pp_indent(pp_fn, expected_type);
            msg += line() + format("this can happen because, for example, coercions were not considered in the process");
            msg += line() + mk_no_overload_applicable_msg(fns, error_msgs);
            throw nested_elaborator_exception(ref, std::current_exception(), msg);
        }
    }

    if (candidates.size() == 1) {
        // Restore successful state
        auto & c = candidates[0];
        expr fn = std::get<0>(c);
        std::get<1>(c).restore(*this);
        first_pass_info & info = std::get<2>(c);
        try {
            return second_pass(fn, args, ref, info);
        } catch (elaborator_exception & ex) {
            auto pp_fn = mk_pp_ctx();
            format msg = format("overload was disambiguated using expected type");
            msg += line() + pp_overloads(pp_fn, fns);
            msg += line() + format("the only applicable one seemed to be: ") + pp_overload(pp_fn, fn);
            msg += line();

            for (auto const & error_msg : error_msgs) {
                msg += line() + error_msg.pp();
            }

            throw nested_elaborator_exception(ref, std::current_exception(), msg);
        }
    }

    try {
        /* Failed to disambiguate using expected type, switch to basic */
        S.restore(*this);
        return visit_overloaded_app_core(fns, args, some_expr(expected_type), ref);
    } catch (elaborator_exception & ex) {
        auto pp_fn = mk_pp_ctx();
        format msg = format("switched to basic overload resolution where arguments are elaborated without "
                            "any information about the expected type because it failed to disambiguate "
                            "overload using the expected type");
        msg += pp_indent(pp_fn, expected_type);
        msg += line() + format("the following overloaded terms were applicable");
        for (auto const & c : candidates)
            msg += pp_indent(pp_fn, std::get<0>(c));
        throw nested_elaborator_exception(ref, std::current_exception(), msg);
    }
}

expr elaborator::visit_overloaded_app(buffer<expr> const & fns, buffer<expr> const & args,
                                      optional<expr> const & expected_type, expr const & ref) {
    trace_elab_detail(tout() << "overloaded application at " << pos_string_for(ref);
                      auto pp_fn = mk_pp_ctx();
                      tout() << pp_overloads(pp_fn, fns) << "\n";);
    if (expected_type) {
        return visit_overloaded_app_with_expected(fns, args, *expected_type, ref);
    } else {
        try {
            return visit_overloaded_app_core(fns, args, expected_type, ref);
        } catch (elaborator_exception & ex) {
            format msg = format("switched to basic overload resolution where arguments are elaborated without "
                                "any information about the expected type because expected type was not available");
            throw nested_elaborator_exception(ref, std::current_exception(), msg);
        }
    }
}

expr elaborator::visit_no_confusion_app(expr const & fn, buffer<expr> const & args, optional<expr> const & expected_type,
                                        expr const & ref) {
    name fn_name = const_name(fn);
    if (!expected_type) {
        throw elaborator_exception(ref, format("invalid '") + format(fn_name) + format ("' application, ") +
                                   format("elaborator has special support for no_confusion ") +
                                   format("but the expected type must be known"));
    }
    if (args.empty()) {
        throw elaborator_exception(ref,
                                   format("invalid occurrence of function '") + format(fn_name) +
                                   format ("', it must be applied to at least one argument (possible solution: use '@')"));
    }
    /* I.no_confusion functions have a type of the form

       Pi (params) (indices) (C : Type) (lhs rhs : I params indices) (H : lhs = rhs),
          I.no_confusion_type params indices C lhs rhs

       The type (I.no_confusion_type params indices C lhs rhs) is C if lhs and rhs are distinct constructors,
       and (Pi Hs, C) if they are the same constructor where Hs is a sequence of equalities.

       (C : Type) is the expected type.

       To make the elaborator more effective, we first elaborate the first explicit argument (i.e., args[0]) and obtain Heq,
       and create the fake pre-term

       (I.no_confusion _ ... _ (as-is expected_type) _ _ (as-is Heq) args[1] ... args[n-1])

       Then, we elaborate this new fake pre-term.
    */
    expr Heq      = strict_visit(args[0], none_expr());
    name I_name   = fn_name.get_prefix();
    inductive_val I_val  = m_env.get(I_name).to_inductive_val();
    unsigned nparams     = I_val.get_nparams();
    unsigned nindices    = I_val.get_nindices();
    buffer<expr> new_args;
    for (unsigned i = 0; i < nparams + nindices; i++) {
        new_args.push_back(copy_pos(ref, mk_expr_placeholder()));
    }
    new_args.push_back(copy_pos(ref, mk_as_is(*expected_type)));
    new_args.push_back(copy_pos(ref, mk_expr_placeholder())); // lhs
    new_args.push_back(copy_pos(ref, mk_expr_placeholder())); // rhs
    new_args.push_back(copy_pos(args[0], mk_as_is(Heq)));
    for (unsigned i = 1; i < args.size(); i++)
        new_args.push_back(args[i]);
    return visit_base_app_core(fn, arg_mask::AllExplicit, new_args, false, expected_type, ref);
}

expr elaborator::visit_app_core(expr fn, buffer<expr> const & args, optional<expr> const & expected_type,
                                expr const & ref) {
    arg_mask amask = arg_mask::Default;
    expr ufn = unwrap_pos(fn);
    if (is_explicit(ufn)) {
        fn   = get_explicit_arg(ufn);
        amask = arg_mask::AllExplicit;
    } else if (is_partial_explicit(ufn)) {
        fn   = get_partial_explicit_arg(ufn);
        amask = arg_mask::InstHoExplicit;
    }

    bool has_args = !args.empty();

    expr orig_fn = fn;
    fn = unwrap_pos(fn);
    while (is_annotation(fn))
        fn = get_annotation_arg(fn);

    if (is_choice(fn)) {
        buffer<expr> fns;
        if (amask != arg_mask::Default) {
            format fmt = format("invalid explicit annotation because of overloading (possible solution: use fully qualified names) ");
            for (unsigned i = 0; i < get_num_choices(fn); i++)
                fns.push_back(get_choice(fn, i));
            auto pp_fn = mk_pp_ctx();
            fmt += pp_overloads(pp_fn, fns);
            throw elaborator_exception(ref, fmt);
        }
        for (unsigned i = 0; i < get_num_choices(fn); i++) {
            expr const & fn_i = get_choice(fn, i);
            fns.push_back(fn_i);
        }
        validate_overloads(fns, ref);
        return visit_overloaded_app(fns, args, expected_type, ref);
    } else if (is_field_notation(fn) && amask == arg_mask::Default) {
        expr s           = visit(mdata_expr(fn), none_expr());
        expr s_type      = head_beta_reduce(instantiate_mvars(infer_type(s)));
        auto field_res   = find_field_fn(fn, s, s_type);
        expr proj, proj_type;
        if (field_res.m_ldecl) {
            proj      = copy_pos(fn, field_res.m_ldecl->mk_ref());
            proj_type = field_res.m_ldecl->get_type();
        } else {
            proj      = copy_pos(fn, mk_constant(field_res.get_full_fname()));
            proj_type = m_env.get(field_res.get_full_fname()).get_type();
        }
        buffer<expr> new_args;
        unsigned i       = 0;
        while (is_pi(proj_type)) {
            if (is_explicit(binding_info(proj_type))) {
                if (is_app_of(extract_mdata(binding_domain(proj_type)), field_res.m_base_S_name)) {
                    /* found s location */
                    expr coerced_s = *mk_base_projections(m_env, field_res.m_S_name, field_res.m_base_S_name, mk_as_is(s));
                    new_args.push_back(copy_pos(fn, std::move(coerced_s)));
                    for (; i < args.size(); i++)
                        new_args.push_back(args[i]);
                    expr new_proj = visit_function(proj, has_args, has_args ? none_expr() : expected_type, ref);
                    return visit_base_app(new_proj, amask, new_args, expected_type, ref);
                } else {
                    if (i >= args.size()) {
                        throw elaborator_exception(ref, sstream() << "invalid field notation, insufficient number of arguments for '"
                                                   << field_res.get_full_fname() << "'");
                    }
                    new_args.push_back(args[i]);
                    i++;
                }
            }
            proj_type = binding_body(proj_type);
        }
        throw elaborator_exception(ref, sstream() << "invalid field notation, function '"
                                   << field_res.get_full_fname() << "' does not have explicit argument with type ("
                                   << field_res.m_base_S_name << " ...)");
    } else {
        expr new_fn = visit_function(fn, has_args, has_args ? none_expr() : expected_type, orig_fn);
        /* Check if we should use a custom elaboration procedure for this application. */
        if (is_constant(new_fn) && amask == arg_mask::Default) {
            if (auto info = use_elim_elab(const_name(new_fn))) {
                if (args.size() >= info->m_nexplicit) {
                    return visit_elim_app(new_fn, *info, args, expected_type, ref);
                } else {
                    try {
                        return visit_base_app(new_fn, amask, args, expected_type, ref);
                    } catch (elaborator_exception & ex) {
                        throw nested_elaborator_exception(ref, std::current_exception(),
                                                          format("'eliminator' elaboration was not used for '") +
                                                          pp(fn) + format("' because it is not fully applied, #") +
                                                          format(info->m_nexplicit) + format(" explicit arguments expected"));
                    }
                }
            } else if (is_no_confusion(m_env, const_name(new_fn))) {
                return visit_no_confusion_app(new_fn, args, expected_type, ref);
            } else {
                try {
                    return visit_base_app(new_fn, amask, args, expected_type, ref);
                } catch (elaborator_exception & ex) {
                    if (auto error_msg = m_elim_failure_info.find(const_name(new_fn))) {
                        throw nested_elaborator_exception(ref, std::current_exception(), *error_msg);
                    } else {
                        throw;
                    }
                }
            }
        }
        return visit_base_app(new_fn, amask, args, expected_type, ref);
    }
}

expr elaborator::visit_local(expr const & e, optional<expr> const & expected_type) {
    return visit_app_core(e, buffer<expr>(), expected_type, e);
}

expr elaborator::visit_constant(expr const & e, optional<expr> const & expected_type) {
    return visit_app_core(e, buffer<expr>(), expected_type, e);
}

expr elaborator::visit_scope_trace(expr const & e, optional<expr> const & expected_type) {
    buffer<expr> new_args;
    auto pip = get_pos_info_provider();
    unsigned line = 0, col = 0;
    if (pip) {
        pos_info pos = pip->get_pos_info_or_some(e);
        line         = pos.first;
        col          = pos.second;
    }
    new_args.push_back(copy_pos(e, mk_expr_placeholder()));
    new_args.push_back(copy_pos(e, mk_prenum(mpz(line))));
    new_args.push_back(copy_pos(e, mk_prenum(mpz(col))));
    new_args.push_back(app_arg(e));
    return visit(mk_app(copy_pos(e, mk_explicit(app_fn(e))), new_args.size(), new_args.data()), expected_type);
}

expr elaborator::visit_app(expr const & e, optional<expr> const & expected_type) {
    if (is_app_of(e, get_scope_trace_name(), 1))
        return visit_scope_trace(e, expected_type);
    buffer<expr> args;
    expr const & fn = get_app_args(e, args);
    flet<optional<pos_info>> _(m_last_pos, get_pos(fn));
    expr ufn = unwrap_pos(fn);
    if (is_infix_function(ufn)) {
        expr infix_fn = get_annotation_arg(ufn);
        lean_assert(is_lambda(infix_fn));
        return visit(head_beta_reduce(copy_pos(e, mk_app(infix_fn, args))), expected_type);
    } else if (is_equations(ufn)) {
        return visit_convoy(e, expected_type);
    } else {
        return visit_app_core(fn, args, expected_type, e);
    }
}

expr elaborator::visit_anonymous_constructor(expr const & e, optional<expr> const & expected_type) {
    lean_assert(is_anonymous_constructor(e));
    buffer<expr> args;
    expr const & c = get_app_args(get_anonymous_constructor_arg(e), args);
    if (!expected_type)
        throw elaborator_exception(e, "invalid constructor ⟨...⟩, expected type must be known");
    expr I = get_app_fn(m_ctx.relaxed_whnf(instantiate_mvars(*expected_type)));
    if (!is_constant(I))
        throw elaborator_exception(e, format("invalid constructor ⟨...⟩, expected type is not an inductive type") +
                                   pp_indent(*expected_type));
    name I_name = const_name(I);
    if (is_private(I_name) && !is_expr_aliased(m_env, I_name))
        throw elaborator_exception(e, "invalid constructor ⟨...⟩, type is a private inductive datatype");
    constant_info I_info = m_env.get(I_name);
    if (!I_info.is_inductive())
        throw elaborator_exception(e, sstream() << "invalid constructor ⟨...⟩, '" << I_name << "' is not an inductive type");
    names c_names = I_info.to_inductive_val().get_cnstrs();
    if (!c_names || tail(c_names))
        throw elaborator_exception(e, sstream() << "invalid constructor ⟨...⟩, '" << I_name << "' must have only one constructor");
    expr type = m_env.get(head(c_names)).get_type();
    unsigned num_explicit = 0;
    while (is_pi(type)) {
        if (is_explicit(binding_info(type)))
            num_explicit++;
        type = binding_body(type);
    }
    if (num_explicit > 1 && args.size() > num_explicit) {
        unsigned num_extra = args.size() - num_explicit;
        expr rest = copy_pos(e, mk_app(c, num_extra + 1, args.data() + num_explicit - 1));
        rest = copy_pos(e, mk_anonymous_constructor(rest));
        args.shrink(num_explicit);
        args.back() = rest;
    }
    expr new_e = copy_pos(e, mk_app(mk_constant(head(c_names)), args));
    return visit(new_e, expected_type);
}

static expr get_equations_fn_type(expr const & eqns) {
    buffer<expr> eqs;
    to_equations(eqns, eqs);
    lean_assert(!eqs.empty());
    lean_assert(is_lambda(eqs[0]));
    return binding_domain(eqs[0]);
}

/** \brief Similar to instantiate_rev, but assumes that subst contains only local constants.
    When replacing a variable with a local, we copy the local constant and inherit the tag
    associated with the variable. This is a trick for preserving position information. */
static expr instantiate_rev_locals(expr const & a, unsigned n, expr const * subst) {
    if (!has_loose_bvars(a))
        return a;
    auto fn = [=](expr const & m, unsigned offset) -> optional<expr> {
        if (offset >= get_loose_bvar_range(m))
            return some_expr(m); // expression m does not contain free variables with idx >= offset
        if (is_var(m)) {
            unsigned vidx = bvar_idx(m).get_small_value();
            if (vidx >= offset) {
                unsigned h = offset + n;
                if (h < offset /* overflow, h is bigger than any vidx */ || vidx < h) {
                    expr local = subst[n - (vidx - offset) - 1];
                    lean_assert(is_local(local));
                    return some_expr(copy_pos(m, copy(local)));
                } else {
                    return some_expr(copy_pos(m, mk_bvar(vidx - n)));
                }
            }
        }
        return none_expr();
    };
    return replace(a, fn);
}

static expr instantiate_rev_locals(expr const & e, type_context_old::tmp_locals const & locals) {
    return instantiate_rev_locals(e, locals.as_buffer().size(), locals.as_buffer().data());
}

static expr update_equations_fn_type(expr const & eqns, expr const & new_fn_type) {
    expr fn_type = mk_as_is(new_fn_type);
    buffer<expr> eqs;
    to_equations(eqns, eqs);
    for (expr & eq : eqs) {
        lean_assert(is_lambda(eq));
        eq = update_binding(eq, fn_type, binding_body(eq));
    }
    return update_equations(eqns, eqs);
}

expr elaborator::visit_convoy(expr const & e, optional<expr> const & expected_type) {
    lean_assert(is_app(e));
    lean_assert(is_equations(unwrap_pos(get_app_fn(e))));
    expr const & ref = e;
    buffer<expr> args, new_args;
    expr eqns = unwrap_pos(get_app_args(e, args));
    lean_assert(equations_num_fns(eqns) == 1);
    lean_assert(equations_size(eqns) > 0);
    expr fn_type = get_equations_fn_type(eqns);
    expr new_fn_type;
    if (is_placeholder(fn_type)) {
        /* User did not provide the type for the match,
           we try to infer one using expected_type and the type of the arguments */
        if (!expected_type)
            throw elaborator_exception(ref, "invalid match/convoy expression, expected type is not known");
        for (unsigned i = 0; i < args.size(); i++)
            new_args.push_back(visit(args[i], none_expr()));
        synthesize();
        new_fn_type = instantiate_mvars(*expected_type);
        unsigned i = args.size();
        while (i > 0) {
            --i;
            expr new_arg      = instantiate_mvars(new_args[i]);
            expr new_arg_type = instantiate_mvars(infer_type(new_arg));
            new_fn_type       = ::lean::mk_pi("_a", new_arg_type, kabstract(m_ctx, new_fn_type, new_arg));
        }
        try {
            check(m_ctx, new_fn_type);
        } catch (exception & ex) {
            throw nested_exception("invalid match/convoy expression, "
                                   "user did not provide type for the expression, "
                                   "lean tried to infer one using expected type information, "
                                   "but result is not type correct", std::current_exception());
        }
    } else {
        // User provided some typing information for the match
        type_context_old::tmp_locals locals(m_ctx);
        expr it = fn_type;
        for (unsigned i = 0; i < args.size(); i++) {
            it = unwrap_pos(it);
            if (!is_pi(it))
                throw elaborator_exception(it, "type expected in match-expression");
            expr d        = instantiate_rev_locals(binding_domain(it), locals);
            expr new_d    = visit(d, none_expr());
            expr ref_d    = get_ref_for_child(binding_domain(it), it);
            new_d         = ensure_type(new_d, ref_d);
            expr expected = replace_locals(new_d, locals.as_buffer(), new_args);
            expr new_arg  = visit(args[i], some_expr(expected));
            new_arg       = enforce_type(new_arg, expected, "type mismatch in match expression", args[i]);
            locals.push_local(binding_name(it), new_d, binding_info(it));
            it            = binding_body(it);
            new_args.push_back(new_arg);
        }
        if (is_placeholder(it)) {
            synthesize();
            if (!expected_type)
                throw elaborator_exception(ref, "invalid match/convoy expression, expected type is not known");
            new_fn_type = instantiate_mvars(*expected_type);
            unsigned i = args.size();
            while (i > 0) {
                --i;
                new_args[i]  = instantiate_mvars(new_args[i]);
                new_fn_type  = instantiate(kabstract(m_ctx, new_fn_type, new_args[i]), locals.as_buffer()[i]);
            }
            new_fn_type = locals.mk_pi(new_fn_type);
        } else {
            expr b      = instantiate_rev_locals(it, locals);
            expr new_b  = visit(b, none_expr());
            synthesize();
            new_fn_type = locals.mk_pi(instantiate_mvars(new_b));
        }
    }
    new_fn_type = instantiate_mvars(new_fn_type);
#if 0
    /* The following check is too restrictive in do blocks. */
    if (has_expr_metavar(new_fn_type)) {
        auto pp_fn = mk_pp_ctx();
        throw elaborator_exception(ref, format("invalid match/convoy expression, type contains meta-variables") +
                                   pp_indent(pp_fn, new_fn_type));
    }
#endif
    trace_elab(tout() << "match/convoy function type: " << new_fn_type << "\n";);
    expr new_eqns = visit_equations(update_equations_fn_type(eqns, new_fn_type));
    expr fn       = get_equations_result(new_eqns, 0);
    return mk_app(fn, new_args);
}

/** \brief Given two binding expressions \c source and \c target
    s.t. they have at least \c num binders, replace the first \c num binders of \c target with \c source.
    The binder types are wrapped with \c mk_as_is to make sure the elaborator will not process
    them again. */
static expr copy_domain(unsigned num, expr const & source, expr const & target) {
    if (num == 0) {
        return target;
    } else if (is_binding(source) && is_binding(target)) {
        return update_binding(source, mk_as_is(binding_domain(source)),
                              copy_domain(num-1, binding_body(source), binding_body(target)));
    } else {
        return target;
    }
}

static void throw_ill_formed_equation(expr const & ref) {
    throw elaborator_exception(ref, "ill-formed match/equation expression");
}

static void check_equations_arity(buffer<expr> const & eqns) {
    buffer<optional<unsigned>> fidx2arity;
    for (expr eqn : eqns) {
        unsigned nbinders = 0;
        expr curr = eqn;
        while (is_lambda(eqn)) {
            nbinders++;
            eqn = binding_body(eqn);
        }
        if (is_equation(eqn)) {
            expr const & lhs = equation_lhs(eqn);
            expr const & fn  = get_app_fn(lhs);
            unsigned arity   = get_app_num_args(lhs);
            if (!is_var(fn) || bvar_idx(fn) >= nbinders)
                throw_ill_formed_equation(eqn);
            unsigned fidx    = nbinders - bvar_idx(fn).get_small_value() - 1;
            if (fidx >= fidx2arity.size())
                fidx2arity.resize(fidx+1, optional<unsigned>());
            if (auto r = fidx2arity[fidx]) {
                if (*r != arity) {
                    throw elaborator_exception(eqn, "invalid match/equations expression, "
                                               "each case must have the same number of patterns");
                }
            } else {
                fidx2arity[fidx] = arity;
            }
        } else if (is_no_equation(eqn)) {
            /* do nothing */
        } else {
            throw_ill_formed_equation(curr);
        }
    }
}

bool elaborator::keep_do_failure_eq(expr const & first_eq) {
    if (!is_lambda(first_eq))
        return false; // possible with error recovery
    expr type = binding_domain(first_eq);
    if (!is_pi(type))
        return false;
    type = binding_body(type);
    if (has_loose_bvars(type))
        return false;
    return is_app(type) && is_monad_fail(app_fn(type));
}

expr elaborator::mk_aux_meta_def(expr const & e, expr const & ref) {
    name aux_name(m_decl_name, "_aux_meta");
    aux_name = aux_name.append_after(m_aux_meta_idx);
    m_aux_meta_idx++;
    expr new_c;
    metavar_context mctx = m_ctx.mctx();
    std::tie(m_env, new_c) = mk_aux_definition(m_env, mctx, local_context(),
                                               aux_name, e, optional<bool>(true));
    if (!is_constant(new_c)) {
        throw elaborator_exception(ref, "failed to create auxiliary definition");
    }
    m_env = compile(m_env, m_opts, const_name(new_c));
    m_ctx.set_env(m_env);
    m_ctx.set_mctx(mctx);
    return new_c;
}

static void mvar_dep_sort_aux(type_context_old & ctx, expr const & m,
                              name_set const & mvar_names, name_set & visited, buffer<expr> & result) {
    if (visited.contains(mvar_name(m)))
        return;
    for_each(ctx.instantiate_mvars(ctx.infer(m)), [&](expr const & e, unsigned) {
            if (is_mvar(e) && mvar_names.contains(mvar_name(e))) {
                mvar_dep_sort_aux(ctx, e, mvar_names, visited, result);
                return false; // do not visit types
            }
            if (is_local(e)) {
                return false; // do not visit types
            }
            return true;
        });
    visited.insert(mvar_name(m));
    result.push_back(m);
}

/* Topological sort based on dependencies. */
static void mvar_dep_sort(type_context_old & ctx, buffer<expr> & mvars) {
    name_set visited;
    buffer<expr> result;
    name_set mvar_names;
    for (expr const & m : mvars)
        mvar_names.insert(mvar_name(m));
    for (expr const & m : mvars)
        mvar_dep_sort_aux(ctx, m, mvar_names, visited, result);
    mvars.clear();
    mvars.append(result);
}

/* We preprocess the equation left-hand-side by first elaborating it using metavariables.
   This process allows us to refine pattern variables whose value is fixed by type inference.
   Before processing the right-hand-side, we must convert back these metavariables into
   pattern variables (i.e., local constants).
   This visitor collects metavariables that must be converted into pattern variables, and
   validates the equation left-hand-side. It returns the input expression expanded to
   expose its primitive patterns. */
class validate_and_collect_lhs_mvars : public replace_visitor {
    elaborator &    m_elab;
    bool            m_has_invalid_pattern = false;
    expr            m_ref;
    /* m_mctx0 is the metavariable context before processing the equation.
       Only metavariables created when processing the equation can be
       converted into pattern variables. */
    metavar_context m_mctx0;
    buffer<expr> &  m_unassigned_mvars;
    name_set        m_collected;

    type_context_old & ctx() { return m_elab.m_ctx; }

    environment const & env() { return m_elab.env(); }

    optional<expr> expand(expr const & e) {
        /* We don't simply use whnf because we want to avoid exposing the internal implementation
           of definitions compiled using the equation compiler */
        {
            /* Try without use delta reduction */
            type_context_old::transparency_scope scope(ctx(), transparency_mode::None);
            expr new_e = ctx().whnf(e);
            if (new_e != e) return some_expr(new_e);
        }
        if (auto new_e = ctx().reduce_projection(e))
            return new_e;
        // HACK: trying to delete `dunfold`
        // /* Try to unfold using refl equations */
        // if (auto new_e = dunfold(ctx(), e))
        // return new_e;
        /* Last resort, whnf using current setting */
        expr new_e = ctx().whnf(e);
        if (new_e != e) return some_expr(new_e);
        return none_expr();
    }

    void throw_invalid_pattern(char const * msg, expr const & e) {
        m_elab.report_or_throw(
            elaborator_exception(m_ref, format(msg)
                + format(" (possible solution, mark term as inaccessible using '.( )')")
                + m_elab.pp_indent(e))
            .ignore_if(m_elab.has_synth_sorry(e)));
        m_has_invalid_pattern = true;
    }

    virtual expr visit_local(expr const & e) override {
        return e;
    }

    virtual expr visit_lambda(expr const & e) override {
        throw_invalid_pattern("invalid occurrence of lambda expression in pattern", e);
        return e;
    }

    virtual expr visit_pi(expr const & e) override {
        throw_invalid_pattern("invalid occurrence of pi/forall expression in pattern", e);
        return e;
    }

    virtual expr visit_let(expr const & e) override {
        return visit(instantiate(let_body(e), let_value(e)));
    }

    virtual expr visit_sort(expr const & e) override {
        throw_invalid_pattern("invalid occurrence of sort in pattern", e);
        return e;
    }

    virtual expr visit_meta(expr const & m) override {
        if (is_metavar_decl_ref(m) && !m_mctx0.find_metavar_decl(m)) {
            if (!m_collected.contains(mvar_name(m))) {
                m_unassigned_mvars.push_back(m);
                m_collected.insert(mvar_name(m));
            }
            return m;
        } else {
            throw_invalid_pattern("invalid occurrence of metavariable in pattern", m);
            return m;
        }
    }

    void throw_invalid_app(expr const & e) {
        if (is_constant(e))
            throw_invalid_pattern("invalid constant in pattern, it cannot be reduced to a constructor", e);
        else
            throw_invalid_pattern("invalid function application in pattern, it cannot be reduced to a constructor", e);
    }

    virtual expr visit_app(expr const & e) override {
        expr it = e;
        while (true) {
            if (is_nat_int_char_string_name_value(ctx(), it))
                return e;
            if (!is_app(it) && !is_constant(it))
                return visit(it);
            buffer<expr> args;
            expr const & fn = get_app_args(it, args);
            if (!is_constant(fn)) {
                throw_invalid_app(e);
                return e;
            }

            constant_info fn_info = env().get(const_name(fn));
            if (fn_info.is_constructor()) {
                unsigned num_params = fn_info.to_constructor_val().get_nparams();
                for (unsigned i = num_params; i < args.size(); i++) {
                    visit(args[i]);
                }
                return e;
            } else {
                if (auto r = expand(it)) {
                    it = *r;
                } else {
                    throw_invalid_app(e);
                    return e;
                }
            }
        }
    }

    virtual expr visit_constant(expr const & e) override {
        return visit_app(e);
    }

    virtual expr visit_mdata(expr const & e) override {
        if (is_inaccessible(e)) {
            return e;
        } else if (is_as_pattern(e)) {
            expr new_lhs = visit(get_as_pattern_lhs(e));
            expr new_rhs = visit(get_as_pattern_rhs(e));
            return mk_as_pattern(new_lhs, new_rhs);
        } else if (is_structure_instance(e)) {
            auto info = get_structure_instance_info(e);
            if (info.m_sources.size())
                throw elaborator_exception(info.m_sources[0], "invalid occurrence of structure notation source in pattern");
            for (expr & val : info.m_field_values)
                val = visit(val);
            return mk_structure_instance(info);
        } else {
            return visit(mdata_expr(e));
        }
    }

public:
    validate_and_collect_lhs_mvars(elaborator & elab, expr const & ref, metavar_context const & mctx0,
                                   buffer<expr> & unassigned_mvars):
        m_elab(elab),
        m_ref(ref),
        m_mctx0(mctx0),
        m_unassigned_mvars(unassigned_mvars) {
    }

    bool operator()(expr const & lhs) {
        buffer<expr> args;
        get_app_args(lhs, args);
        for (expr & arg : args)
            visit(arg);
        return m_has_invalid_pattern;
    }
};

/* Similar to instantiate_mvars, but add an inaccessible pattern annotation around metavariables
   whose value has been fixed by type inference. */
static expr instantiate_pattern_mvars(type_context_old & ctx, expr const & lhs) {
    return replace(lhs, [&](expr const & e, unsigned) {
            if (is_metavar_decl_ref(e) && ctx.is_assigned(e)) {
                expr v = ctx.instantiate_mvars(e);
                if (!is_local(v) && !is_metavar(v))
                    return some_expr(copy_pos(e, mk_inaccessible(v)));
                else
                    return some_expr(v);
            } else {
                return none_expr();
            }
        });
}

expr elaborator::visit_equation(expr const & e, unsigned num_fns) {
    expr const & ref = e;
    type_context_old::tmp_locals fns(m_ctx);
    expr it = e;
    for (unsigned i = 0; i < num_fns; i++) {
        if (!is_lambda(it))
            throw_ill_formed_equation(ref);
        expr d     = instantiate_rev_locals(binding_domain(it), fns);
        expr new_d = visit(d, none_expr());
        expr ref_d = get_ref_for_child(binding_domain(it), it);
        expr fn    = push_local(fns, binding_name(it), new_d, binding_info(it), ref_d);
        it = binding_body(it);
    }
    if (is_no_equation(it)) {
        return fns.mk_lambda(it);
    } else {
        trace_elab_equation(tout() << "visit_equation\n";);
        metavar_context mctx0 = m_ctx.mctx();
        it = instantiate_rev_locals(it, fns);
        buffer<expr> local_mvars;
        buffer<expr> type_mvars;
        while (is_lambda(it)) {
            expr type = mk_type_metavar(it);
            type_mvars.push_back(type);
            expr mvar = m_ctx.mk_metavar_decl(binding_name(it), m_ctx.lctx(), type);
            local_mvars.push_back(mvar);
            it = binding_body(it);
        }
        if (!is_equation(it))
            throw_ill_formed_equation(ref);
        expr lhs    = instantiate_rev(equation_lhs(it), local_mvars);
        expr lhs_fn = get_app_fn(lhs);
        if (is_explicit_or_partial_explicit(lhs_fn))
            lhs_fn = get_explicit_or_partial_explicit_arg(lhs_fn);
        if (!is_local_p(lhs_fn))
            throw_ill_formed_equation(ref);
        expr new_lhs;
        {
            flet<bool> set(m_in_pattern, true);
            flet<bool> dont_recover(m_recover_from_errors, false);
            /* Note that pattern elaboration is more sensitive than standard elaboration:
             * mvars in `new_lhs` will be turned into pattern variables below, so care must be taken when instantiating
             * or introducing them. See the very end of `visit_structure_instance` for an example. */
            new_lhs = visit(lhs, none_expr());
            synthesize();
        }
        trace_elab_equation(tout() << "new_lhs:\n" << new_lhs << "\n";);
        new_lhs = instantiate_pattern_mvars(m_ctx, new_lhs);
        trace_elab_equation(tout() << "new_lhs (after instantiate_pattern_mvars):\n" << new_lhs << "\n";);
        // collect unassigned metavariables not in mctx0
        buffer<expr> unassigned_mvars;
        validate_and_collect_lhs_mvars(*this, ref, mctx0, unassigned_mvars)(new_lhs);
        // sort using dependencies
        mvar_dep_sort(m_ctx, unassigned_mvars);
        // create local variables for each unassigned metavar
        type_context_old::tmp_locals new_locals(m_ctx);
        unsigned next_idx = 1;
        for (expr & m : unassigned_mvars) {
            expr type      = instantiate_mvars(m_ctx.infer(m));
            name user_name;
            if (optional<metavar_decl> mdecl = m_ctx.find_metavar_decl(m)) {
                user_name = mdecl->get_user_name();
            } else {
                user_name = name("_x").append_after(next_idx);
                next_idx++;
            }
            expr new_local = new_locals.push_local(user_name, type, mk_binder_info());
            m_ctx.assign(m, new_local);
        }
        // replace metavariables with new locals
        new_lhs           = instantiate_mvars(new_lhs);
        expr new_lhs_type = instantiate_mvars(infer_type(new_lhs));
        // process rhs
        buffer<expr> rhs_subst;
        for (expr const & local_mvar : local_mvars) {
            expr s = instantiate_mvars(local_mvar);
            if (!is_local(s)) {
                /* The `as_is` annotation affects how applications are elaborated.
                   See comment at first_pass method.
                   So, we only use it if it is really needed. That is,
                   the metavariable was fixed to a non trivial term by type inference rules.
                   For example, in the following definition

                   ```
                   inductive Vec (A : Type) : nat → Type
                   | nil {} : Vec 0
                   | cons   : Π {n}, A → Vec n → Vec (succ n)

                   open Vec

                   def append1 {A : Type} : Π {m n : nat}, Vec A m -> Vec A n -> Vec A (n + m)
                   | n m nil         ys := ys
                   | n m (cons x xs) ys := cons x (append1 xs ys)
                   ```

                   The variable n is refined to 0 and (succ n') in the first and second equations.
                */
                s = mk_as_is(s);
            }
            rhs_subst.push_back(s);
        }
        expr rhs     = instantiate_rev(equation_rhs(it), rhs_subst);
        expr new_rhs = visit(rhs, some_expr(new_lhs_type));
        // synthesize();
        // new_rhs       = instantiate_mvars(new_rhs);
        new_rhs       = enforce_type(new_rhs, new_lhs_type, "equation type mismatch", it);
        expr new_eq = copy_pos(it, mk_equation(new_lhs, new_rhs, ignore_equation_if_unused(it)));
        trace_elab_equation(tout() << new_eq << "\n";);
        expr r = copy_pos(ref, fns.mk_lambda(new_locals.mk_lambda(new_eq)));
        return r;
    }
}

expr elaborator::visit_equations(expr const & e) {
    expr const & ref = e;
    buffer<expr> eqs;
    buffer<expr> new_eqs;
    optional<expr> new_tacs;
    equations_header const & header = get_equations_header(e);
    unsigned num_fns = header.m_num_fns;
    to_equations(e, eqs);
    lean_assert(!eqs.empty());

    if (is_wf_equations(e)) {
        expr type = mk_constant(get_well_founded_tactics_name());
        new_tacs  = visit(equations_wf_tactics(e), some_expr(type));
        new_tacs  = enforce_type(*new_tacs, type, "well_founded_tactics object expected", ref);
        if (!is_constant(*new_tacs)) {
            new_tacs = mk_aux_meta_def(*new_tacs, ref);
        }
    }

    optional<expr> first_eq;
    for (expr const & eq : eqs) {
        expr new_eq;
        if (first_eq) {
            if (is_do_failure_eq(eq) && !keep_do_failure_eq(*first_eq)) {
                /* skip equation since it doesn't implement the monad_fail interface */
            } else {
                /* Replace first num_fns domains of eq with the ones in first_eq.
                   This is a trick/hack to ensure the fns in each equation have
                   the same elaborated type. */
                new_eq   = copy_pos(eq, visit_equation(copy_domain(num_fns, *first_eq, eq), num_fns));
                new_eqs.push_back(new_eq);
            }
        } else {
            new_eq   = copy_pos(eq, visit_equation(eq, num_fns));
            first_eq = new_eq;
            new_eqs.push_back(new_eq);
        }
    }
    check_equations_arity(new_eqs);
    synthesize();
    expr new_e;
    if (new_tacs) {
        new_e = copy_pos(e, mk_equations(header, new_eqs.size(), new_eqs.data(), *new_tacs));
    } else {
        new_e = copy_pos(e, mk_equations(header, new_eqs.size(), new_eqs.data()));
    }
    new_e = instantiate_mvars(new_e);
    ensure_no_unassigned_metavars_core(new_e);
    if (has_sorry(new_e))
        // aborting here prevents many additional errors
        throw elaborator_exception(e, "[abort visit_equation]").ignore_if(true);
    if (!has_synth_sorry(new_e) && has_expr_metavar(e)) {
        /* We perform this check to be able to reject code such as:
           ```
           @[pattern] def badPattern (x : Nat) : Nat := 0
           def tst (x : Nat) : Nat :=
           match x with
           | (@badPattern _) := 1
           | _               := 2
           ```
        */
        // TODO(Leo, Kha): enable the following check
        // throw elaborator_exception(new_e, "invalid pattern");
    }
    metavar_context mctx = m_ctx.mctx();
    expr r = compile_equations(m_env, *this, mctx, m_ctx.lctx(), new_e);
    m_ctx.set_env(m_env);
    m_ctx.set_mctx(mctx);
    return r;
}

expr elaborator::visit_inaccessible(expr const & e, optional<expr> const & expected_type) {
    if (!m_in_pattern)
        throw elaborator_exception(e, "invalid occurrence of 'inaccessible' annotation, "
                                   "it must only occur in patterns");
    expr a = get_annotation_arg(e);
    expr new_a;
    {
        flet<bool> set(m_in_pattern, false);
        new_a = visit(a, expected_type);
    }
    return copy_pos(e, mk_inaccessible(new_a));
}

elaborator::field_resolution elaborator::field_to_decl(expr const & e, expr const & s, expr const & s_type) {
    // prefer 'unknown identifier' error when lhs is a constant of non-value type
    if (is_field_notation(e)) {
        auto lhs = unwrap_pos(mdata_expr(e));
        if (is_constant(lhs)) {
            type_context_old::tmp_locals locals(m_ctx);
            expr t = whnf(s_type);
            while (is_pi(t)) {
                t = whnf(instantiate(binding_body(t), locals.push_local_from_binding(t)));
            }
            if (is_sort(t) && !is_anonymous_field_notation(e)) {
                name fname = get_field_notation_field_name(e);
                throw elaborator_exception(lhs, format("unknown identifier '") + format(const_name(lhs)) + format(".") +
                                           format(fname) + format("'"));
            }
        }
    }
    expr I      = get_app_fn(s_type);
    if (!is_constant(I)) {
        auto pp_fn = mk_pp_ctx();
        throw elaborator_exception(e, format("invalid field notation, type is not of the form (C ...) where C is a constant") +
                                   pp_indent(pp_fn, s) +
                                   line() + format("has type") +
                                   pp_indent(pp_fn, s_type));
    }
    if (is_anonymous_field_notation(e)) {
        if (!is_structure(m_env, const_name(I))) {
            auto pp_fn = mk_pp_ctx();
            throw elaborator_exception(e, format("invalid projection, structure expected") +
                                       pp_indent(pp_fn, s) +
                                       line() + format("has type") +
                                       pp_indent(pp_fn, s_type));
        }
        auto fnames = get_structure_fields(m_env, const_name(I));
        unsigned fidx = get_field_notation_field_idx(e);
        if (fidx  == 0) {
            throw elaborator_exception(e, "invalid projection, index must be greater than 0");
        }
        if (fidx > fnames.size()) {
            auto pp_fn = mk_pp_ctx();
            throw elaborator_exception(e, format("invalid projection, structure has only ") +
                                       format(fnames.size()) + format(" field(s)") +
                                       pp_indent(pp_fn, s) +
                                       line() + format("which has type") +
                                       pp_indent(pp_fn, s_type));
        }
        return const_name(I) + fnames[fidx-1];
    } else {
        name fname  = get_field_notation_field_name(e);
        // search for "true" fields first, including in parent structures
        if (is_structure_like(m_env, const_name(I)))
            if (auto p = find_field(m_env, const_name(I), fname))
                return field_resolution(const_name(I), *p, fname);
        name full_fname = const_name(I) + fname;
        name local_name = full_fname.replace_prefix(get_namespace(env()), {});
        if (auto ldecl = m_ctx.lctx().find_if([&](local_decl const & decl) {
            return is_rec(decl.get_info()) && decl.get_user_name() == local_name;
        })) {
            // projection is recursive call
            return field_resolution(full_fname, ldecl);
        }
        if (!m_env.find(full_fname)) {
            auto pp_fn = mk_pp_ctx();
            throw elaborator_exception(e, format("invalid field notation, '") + format(fname) + format("'") +
                                       format(" is not a valid \"field\" because environment does not contain ") +
                                       format("'") + format(full_fname) + format("'") +
                                       pp_indent(pp_fn, s) +
                                       line() + format("which has type") +
                                       pp_indent(pp_fn, s_type));
        }
        return full_fname;
    }
}

elaborator::field_resolution elaborator::find_field_fn(expr const & e, expr const & s, expr const & s_type) {
    try {
        return field_to_decl(e, s, s_type);
    } catch (elaborator_exception & ex1) {
        expr new_s_type = s_type;
        if (auto d = unfold_term(env(), new_s_type))
            new_s_type = *d;
        new_s_type = m_ctx.whnf_head_pred(new_s_type, [](expr const &) { return false; });
        if (new_s_type == s_type)
            throw;
        std::exception_ptr ex1_ptr = std::current_exception();
        try {
            return find_field_fn(e, s, new_s_type);
        } catch (elaborator_exception & ex2) {
            throw nested_elaborator_exception(ex2.get_pos(), ex1_ptr, ex2.pp());
        }
    }
}

expr elaborator::visit_field(expr const & e, optional<expr> const & expected_type) {
    lean_assert(is_field_notation(e));
    expr s         = visit(mdata_expr(e), none_expr());
    expr s_type    = head_beta_reduce(instantiate_mvars(infer_type(s)));
    auto field_res = find_field_fn(e, s, s_type);
    expr proj_app;
    if (field_res.m_ldecl) {
        proj_app   = copy_pos(e, mk_app(field_res.m_ldecl->mk_ref(), mk_as_is(s)));
    } else {
        expr new_e = *mk_base_projections(m_env, field_res.m_S_name, field_res.m_base_S_name, mk_as_is(s));
        proj_app   = mk_proj_app(m_env, field_res.m_base_S_name, field_res.m_fname, new_e, e);
    }
    return visit(proj_app, expected_type);
}

class reduce_projections_visitor : public replace_visitor {
private:
    type_context_old & m_ctx;
protected:
    expr visit_app(expr const & e) override {
        expr e2 = replace_visitor::visit_app(e);
        if (has_metavar(e2)) {
            if (auto e3 = m_ctx.reduce_projection(e2))
                return *e3;
        }
        return e2;
    }
public:
    reduce_projections_visitor(type_context_old & ctx): m_ctx(ctx) {}
};

/* Predicated variant of `lean::instantiate_mvars`. It does not support universe mvars. */
expr elaborator::instantiate_mvars(expr const & e, std::function<bool(expr const &)> pred) { // NOLINT
    return replace(e, [&](expr const & e) {
        if (m_ctx.is_mvar(e) && pred(e))
            if (auto asn = m_ctx.get_assignment(e))
                return some_expr(instantiate_mvars(*asn, pred));
        return none_expr();
    });
}

/** Elaborate the structure instance notation `{... := ..., ...}` */
class visit_structure_instance_fn {
    // an elaborated source `..(m_e : m_S_name ...)`
    struct source {
        expr m_e;
        // known to be a structure
        name m_S_name;
    };

    elaborator & m_elab;
    // note: fields needed by trace macros
    environment & m_env = m_elab.m_env;
    type_context_old & m_ctx = m_elab.m_ctx;

    // inputs
    expr m_e, m_ref = m_e;
    optional<expr> m_expected_type;
    bool m_use_subobjects = !m_elab.m_opts.get_bool("old_structure_cmd", false);
    structure_instance_info m_info = get_structure_instance_info(m_e);
    name & m_S_name = m_info.m_struct_name;
    buffer<name> const & m_fnames = m_info.m_field_names;
    buffer<expr> const & m_fvalues = m_info.m_field_values;

    // set of fnames that have been used by `create_field_mvars`. Not using all of them is an error.
    name_set m_fnames_used;
    // fields for which no value could be found
    buffer<name> m_missing_fields;
    // elaborated sources
    buffer<source> m_sources;

    /* Because field default values may introduce arbitrary dependencies on other field values (not only within a structure,
     * but even between a structure and its parent structures, in either direction), we cannot elaborate fields in a static
     * order. Instead, we register a metavar and an elaboration function for each field (create_field_mvars).
     * We call the elaboration function and substitute the metavar with its result (insert_field_values)
     * only after all field metavars in its expected type have been resolved (reduce_and_check_dependencies). */

    // field -> elaboration function
    name_map<std::function<expr(expr const & expected_type)>> m_field2elab;
    // field <-> metavar for each field to be elaborated
    name_map<expr> m_field2mvar;
    name_map<name> m_mvar2field;

    bool field_from_source(name const & S_fname) {
        for (source const & src : m_sources) {
            if (optional<name> base_S_name = find_field(m_env, src.m_S_name, S_fname)) {
                expr base_src = *mk_base_projections(m_env, src.m_S_name, *base_S_name, src.m_e);
                expr f = mk_proj_app(m_env, *base_S_name, S_fname, base_src);
                m_field2elab.insert(S_fname, [=](expr const & d) {
                    return m_elab.visit(f, some_expr(d));
                });
                return true;
            }
        }
        return false;
    }

    struct field_not_ready_to_synthesize_exception : exception {
        std::function<format()> m_fmt;
        explicit field_not_ready_to_synthesize_exception(std::function<format()> m_fmt) : m_fmt(m_fmt) {}
    };

    void field_from_default_value(name const & S_fname) {
        m_field2elab.insert(S_fname, [=](expr const & d) {
            if (m_elab.m_in_pattern) {
                // insert placeholder during pattern elaboration
                return m_elab.mk_metavar(d, m_ref);
            } else {
                name full_S_fname = m_S_name + S_fname;
                expr pre_fval = mk_field_default_value(m_env, full_S_fname, [&](name const & fname) {
                    // just insert mvars for now, we will check for any uninstantiated ones in `reduce_and_check_deps` below
                    return m_field2mvar[fname];
                });
                expr fval = m_elab.visit(pre_fval, some_expr(d));
                /* Delta- and beta-reduce `_default` definition. This can remove dependencies when defaulting
                 * a field of a parent structure.
                 * For example, monad defaults applicative.seq in terms of map and bind. However, since map is part
                 * of the subobject to_applicative as well, we get the recursive constraint
                 * `?seq =?= monad.seq._default {to_functor := {map := ?map, ...}, seq := ?seq, ...} ?bind ...`
                 * Reducing monad.seq._default allows `reduce_and_check_deps` below to remove the ?seq dependency. */
                buffer<expr> args;
                expr fn = get_app_args(fval, args);
                if (is_constant(fn)) {
                    constant_info decl = m_env.get(const_name(fn));
                    expr default_val = instantiate_value_lparams(decl, const_levels(fn));
                    // clean up 'id' application inserted by `structure_cmd::declare_defaults`
                    default_val = replace(default_val, [](expr const & e) {
                        if (is_app_of(e, get_id_name(), 2)) {
                            return some_expr(app_arg(e));
                        }
                        return none_expr();
                    });
                    fval = mk_app(default_val, args);
                    fval = head_beta_reduce(fval);
                }
                reduce_and_check_deps(fval, full_S_fname);
                return fval;
            }
        });
    }

    /** Recurse over structure hierarchy and build structure expression, introducing mvars for each parameter and
        leaf field.

        Example: `create_field_mvars('applicative') =
                    (applicative.mk ?p1 (functor.mk ?p2 ?f1 ?f2...) ?f3..., applicative ?p1)` */
    std::pair<expr, expr> create_field_mvars(name const & nested_S_name) {
        buffer<name> c_names;
        get_constructor_names(m_env, nested_S_name, c_names);
        lean_assert(c_names.size() == 1);
        expr c = m_elab.visit_const_core(copy_pos(m_e, mk_constant(c_names[0])));
        buffer<expr> c_args;
        expr c_type = m_elab.infer_type(c);
        unsigned nparams = m_env.get(nested_S_name).to_inductive_val().get_nparams();

        // iterate over nested_S_name's constructor parameters
        for (unsigned i = 0; is_pi(c_type); i++) {
            name S_fname = deinternalize_field_name(binding_name(c_type));
            expr c_arg;
            expr d = binding_domain(c_type);

            if (i < nparams) {
                /* struct type parameter */
                if (is_explicit(binding_info(c_type)) && !m_expected_type) {
                    m_elab.report_or_throw(elaborator_exception(m_e, sstream()
                            << "invalid structure value {...}, structure parameter '" <<
                            binding_name(c_type)
                            << "' is explicit in the structure constructor '" <<
                            c_names[0] << "' and the expected type is not known"));
                }
                c_arg = m_elab.mk_metavar(d, m_ref);
            } else {
                /* struct field */
                c_arg = m_elab.mk_metavar(S_fname, d, m_ref);

                /* Try to find field value, in the following order:
                 * 1) explicit value from m_fvalues
                 * 2) subobject field: recurse
                 * 3) value from source
                 * 4) opt/auto/implicit param
                 * 6) `..` catchall: placeholder
                 */
                auto it = std::find(m_fnames.begin(), m_fnames.end(), S_fname);
                if (it != m_fnames.end()) {
                    /* explicitly passed field */
                    m_fnames_used.insert(S_fname);
                    expr const & p = m_fvalues[it - m_fnames.begin()];
                    m_field2elab.insert(S_fname, [=](expr const & d) {
                        return m_elab.visit(p, some_expr(consume_auto_opt_param(d)));
                    });
                } else if (auto p = is_subobject_field(m_env, nested_S_name, S_fname)) {
                    /* subobject field */
                    auto num_used = m_fnames_used.size();
                    auto old_missing_fields = m_missing_fields;
                    auto nested = create_field_mvars(*p);
                    if (m_fnames_used.size() == num_used && field_from_source(S_fname)) {
                        // If the subobject doesn't contain any explicitly passed fields, we prefer to use
                        // its value directly from a source so that the two are definitionally equal
                    } else if (!is_explicit(binding_info(c_type)) && m_fnames_used.size() == num_used &&
                            old_missing_fields.size() < m_missing_fields.size()) {
                        // If the subobject is a superclass, doesn't contain any explicitly passed fields,
                        // and has missing fields, we fall back to type inference
                        m_field2elab.insert(S_fname, [=](expr const & d) {
                            return m_elab.mk_instance(d, m_ref);
                        });
                        m_missing_fields = old_missing_fields;
                    } else {
                        // We assign the subtree to the mvar eagerly so that the mvars representing the nested
                        // structure parameters are assigned, which are not inlcuded in the m_mvar2field dependency
                        // tracking
                        lean_always_assert(m_elab.is_def_eq(c_arg, nested.first));
                        // should always hold because `nested.first` is a constructor application
                        lean_always_assert(m_elab.is_mvar_assigned(c_arg));
                    }
                } else if (field_from_source(S_fname)) {
                } else if (has_default_value(m_env, m_S_name, S_fname)) { // note: S_name instead of nested_S_name
                    field_from_default_value(S_fname);
                } else if (is_auto_param(d)) {
                    /* auto param */
                    m_field2elab.insert(S_fname, [=](expr const & d) {
                        if (m_elab.m_in_pattern) {
                            // insert placeholder during pattern elaboration
                            return m_elab.mk_metavar(d, m_ref);
                        } else {
                            auto p = is_auto_param(d);
                            return m_elab.mk_auto_param(p->second, p->first, m_ref);
                        }
                    });
                } else if (!is_explicit(binding_info(c_type))) {
                    /* implicit field */
                    m_field2elab.insert(S_fname, [=](expr const & d) {
                        return is_inst_implicit(binding_info(c_type)) ? m_elab.mk_instance(d, m_ref) : m_elab.mk_metavar(d, m_ref);
                    });
                } else if (m_info.m_catchall) {
                    /* catchall: insert placeholder */
                    m_field2elab.insert(S_fname, [=](expr const & d) {
                        return m_elab.mk_metavar(d, m_ref);
                    });
                } else {
                    /* failure */
                    // Do not log an error immediately because subobjects may need backtracking
                    m_missing_fields.push_back(S_fname);
                    m_field2elab.insert(S_fname, [=](expr const & d) {
                        return m_elab.mk_sorry(some_expr(d), m_ref);
                    });
                }

                m_field2mvar.insert(S_fname, c_arg);
                m_mvar2field.insert(mvar_name(c_arg), S_fname);
            }

            c_args.push_back(c_arg);
            c_type = instantiate(binding_body(c_type), c_arg);
        }
        return mk_pair(mk_app(c, c_args), c_type);
    }

    /** Check `e` for dependencies on fields that have not been inserted yet.
     * Also reduce projections containing mvars, which may remove dependencies.
     * Example: `functor.map (functor.mk ?p1 ?m1 ?m2...) => ?m1`
     */
    void reduce_and_check_deps(expr & e, name const & full_S_fname) {
        if (m_use_subobjects)
            e = reduce_projections_visitor(m_ctx)(e);
        name_set deps;
        e = m_elab.instantiate_mvars(e);
        for_each(e, [&](expr const & e, unsigned) {
            name const *n;
            if (is_metavar(e) && (n = m_mvar2field.find(mvar_name(e))) && !m_ctx.is_assigned(e))
                deps.insert(*n);
            return has_expr_metavar(e);
        });
        if (!deps.empty()) {
            throw field_not_ready_to_synthesize_exception([=]() {
                format error = format("Failed to insert value for '") + format(full_S_fname) +
                               format("', it depends on field(s) '");
                bool first = true;
                deps.for_each([&](name const & dep) {
                    if (!first) error += format("', '");
                    error += format(dep);
                    first = false;
                });
                error += format("', but the value for these fields is not available.") + line() +
                         format("Unfolded type/default value:") + line() +
                         pp_until_meta_visible(m_elab.mk_fmt_ctx(), e) + line() +
                         line();
                return error;
            });
        }
    }

    void elaborate_sources() {
        for (expr src : m_info.m_sources) {
            lean_assert(!m_elab.m_in_pattern);
            src = m_elab.visit(src, none_expr());
            m_elab.synthesize_type_class_instances();
            expr type = m_elab.instantiate_mvars(m_elab.whnf(m_elab.infer_type(src)));
            expr src_S = get_app_fn(type);
            if (!is_constant(src_S) || !is_structure(m_env, const_name(src_S))) {
                auto pp_fn = m_elab.mk_pp_ctx();
                m_elab.report_or_throw(elaborator_exception(m_e,
                                                            format("invalid structure notation source, not a structure") +
                                                            m_elab.pp_indent(pp_fn, src) +
                                                            line() + format("which has type") +
                                                            m_elab.pp_indent(pp_fn, type)));
            } else {
                m_sources.push_back(source {copy_pos(src, mk_as_is(src)), const_name(src_S)});
            }
        }
    }

    void find_structure_name() {
        if (m_expected_type) {
            expr type = m_elab.whnf(*m_expected_type);
            expr S = get_app_fn(type);
            if (!is_constant(S) || !is_structure(m_env, const_name(S))) {
                auto pp_fn = m_elab.mk_pp_ctx();
                throw elaborator_exception(m_e,
                                           format("invalid structure value {...}, expected type is known, "
                                                          "but it is not a structure") +
                                           m_elab.pp_indent(pp_fn, *m_expected_type));
            }
            m_S_name = const_name(S);
        } else if (m_sources.size() == 1 && !m_info.m_catchall) {
            m_S_name = m_sources[0].m_S_name;
        } else {
            throw elaborator_exception(m_e, "invalid structure value {...}, expected type is not known"
                    "(solution: use qualified structure instance { struct_id . ... }");
        }
    }

    /** Repeatedly try to elaborate fields whose dependencies have been elaborated.
      * If we have not made any progress in a round, do a last one collecting any errors. */
    void insert_field_values(expr const & e) {
        bool done = false;
        bool last_progress = true;

        while (!done) {
            done = true;
            bool progress = false;
            format error;
            // Try to resolve helper metavars reachable from e. Note that `m_mvar2field` etc. may contain
            // metavars unreachable from e because of backtracking.
            expr e2 = m_elab.instantiate_mvars(e);
            for_each(e2, [&](expr const & e, unsigned) {
                if (is_metavar(e) && m_mvar2field.contains(mvar_name(e))) {
                    name S_fname = m_mvar2field[mvar_name(e)];
                    name full_S_fname = m_S_name + S_fname;
                    expr expected_type = m_elab.infer_type(e);
                    expr reduced_expected_type = m_elab.instantiate_mvars(expected_type);
                    expr val;

                    try {
                        reduce_and_check_deps(reduced_expected_type, full_S_fname);
                        /* note: we pass the reduced, mvar-free expected type. Otherwise auto params may fail with
                         * "result contains meta-variables". */
                        val = (*m_field2elab.find(S_fname))(reduced_expected_type);
                    } catch (field_not_ready_to_synthesize_exception const & e) {
                        done = false;
                        if (!last_progress)
                            error += e.m_fmt();
                        return true;
                    }

                    expr val_type = m_elab.infer_type(val);
                    if (auto val2 = m_elab.ensure_has_type(val, val_type, expected_type, m_ref)) {
                        /* Make sure mvar is assigned, even if val2 is a meta var as well.
                         * This is important for termination and the `instantiate_mvars` call in `operator()`.
                         * Note that `ensure_has_type` has already unified their types, so this should not result
                         * in any missed unifications.
                         */
                        lean_always_assert(m_ctx.match(e, *val2));
                        trace_elab_detail(tout() << "inserted field '" << S_fname << "' with value '" << *val2 << "'"
                                                 << "\n";)
                        progress = true;
                    } else {
                        format msg = format("type mismatch at field '") + format(S_fname) + format("'");
                        msg += m_elab.pp_type_mismatch(val, val_type, expected_type);
                        throw elaborator_exception(val, msg);
                    }
                }
                return has_metavar(e);
            });
            if (!last_progress && !progress)
                throw elaborator_exception(m_ref, error);
            last_progress = progress;
        }
    }
public:
    visit_structure_instance_fn(elaborator & m_elab, expr const & e, optional<expr> const & expected_type) :
            m_elab(m_elab), m_e(e), m_expected_type(expected_type) {}

    expr operator()() {
        if (!m_S_name.is_anonymous() && !is_structure(m_elab.env(), m_S_name))
            throw elaborator_exception(m_e, sstream() << "invalid structure instance, '" <<
                                                    m_S_name << "' is not the name of a structure type");
        lean_assert(m_fnames.size() == m_fvalues.size());

        elaborate_sources();

        if (m_S_name.is_anonymous())
            find_structure_name();

        if (is_private(m_S_name) && !is_expr_aliased(m_env, m_S_name))
            throw elaborator_exception(m_e, "invalid structure instance, type is a private structure");

        expr e2, c_type;
        std::tie(e2, c_type) = create_field_mvars(m_S_name);

        /* Report missing fields. */
        for (name const & S_fname : m_missing_fields) {
            m_elab.report_or_throw(elaborator_exception(m_e, sstream() << "invalid structure value { ... }, field '"
                                                                       << S_fname << "' was not provided"));
        }

        /* Check if there are alien fields. */
        for (name const & fname : m_fnames) {
            if (!m_fnames_used.contains(fname)) {
                m_elab.report_or_throw(
                        elaborator_exception(m_e, sstream() << "invalid structure value { ... }, '" << fname << "'"
                                                          << " is not a field of structure '" << m_S_name << "'"));
            }
        }

        /* Make sure to unify first to propagate the expected type, we'll report any errors later on. */
        bool type_def_eq = !m_expected_type || m_elab.is_def_eq(*m_expected_type, c_type);

        insert_field_values(e2);

        /* Check expected type */
        if (!type_def_eq) {
            throw elaborator_exception(m_ref, format("type mismatch as structure instance") +
                                            m_elab.pp_type_mismatch(e2, c_type, *m_expected_type));
        }

        if (m_elab.m_in_pattern) {
            /* Instantiate all helper mvars introduced by this function. This is important when elaborating patterns because
             * all mvars left in the final expression are turned into pattern variables by `visit_equation` (see there).
             * For example, the pattern `{a := a}` will result in the input `m_e = {a := ?a}` and result `e2 = foo.mk ?m` with
             * the assignment `?m := ?a` from field elaboration. We want the return value to be `foo.mk ?a` regardless of
             * whether ?a has an assignment (from a dependent pattern) or not. */
            e2 = m_elab.instantiate_mvars(e2, [&](expr const & e) { return m_mvar2field.contains(mvar_name(e)); });
        }
        return e2;
    }
};

expr elaborator::visit_structure_instance(expr const & e, optional<expr> expected_type) {
    synthesize_type_class_instances();
    if (expected_type) {
        expected_type = instantiate_mvars(*expected_type);
        if (is_metavar(*expected_type))
            expected_type = none_expr();
    }
    return visit_structure_instance_fn(*this, e, expected_type)();
}

expr elaborator::visit_mdata(expr const & e, optional<expr> const & expected_type, bool is_app_fn) {
    if (is_as_is(e)) {
        return get_as_is_arg(e);
    } else if (is_choice(e)) {
        return visit_app_core(e, buffer<expr>(), expected_type, e);
    } else if (is_explicit(e) || is_partial_explicit(e)) {
        return visit_app_core(e, buffer<expr>(), expected_type, e);
    } else if (is_anonymous_constructor(e)) {
        if (is_app_fn)
            throw elaborator_exception(e, "invalid constructor ⟨...⟩, function expected");
        return visit_anonymous_constructor(e, expected_type);
    } else if (is_as_atomic(e)) {
        /* ignore annotation */
        expr new_e = visit(get_as_atomic_arg(e), none_expr());
        if (is_app_fn)
            return new_e;
        /* If the as_atomic is not the the function in a function application, then we need to consume
           implicit arguments. */
        return visit_base_app_core(new_e, arg_mask::Default, buffer<expr>(), true, expected_type, e);
    } else if (is_inaccessible(e)) {
        if (is_app_fn)
            throw elaborator_exception(e, "invalid inaccessible term, function expected");
        return visit_inaccessible(e, expected_type);
    } else if (is_frozen_name(e)) {
        return visit(get_annotation_arg(e), expected_type);
    } else if (is_structure_instance(e)) {
        return visit_structure_instance(e, expected_type);
    } else if (is_field_notation(e)) {
        return visit_field(e, expected_type);
    } else if (is_as_pattern(e)) {
        if (!m_in_pattern)
            throw elaborator_exception(e, "invalid occurrence of aliasing pattern, it must only occur in patterns");
        expr new_rhs = visit(get_as_pattern_rhs(e), expected_type);
        expr lhs = get_as_pattern_lhs(e);
        if (!is_def_eq(lhs, new_rhs))
            throw elaborator_exception(e, "cannot unify terms of aliasing pattern");
        return new_rhs;
    } else if (is_equation(e)) {
        throw elaborator_exception(e, "unexpected occurrence of equation");
    } else if (is_equations(e)) {
        lean_assert(!is_app_fn); // visit_convoy is used in this case
        return visit_equations(e);
    } else {
        expr new_e = visit(mdata_expr(e), expected_type);
        return update_mdata(e, new_e);
    }
}

/* If the instance fingerprint has been set, then make sure `type` is not a local instance.
   Then, add a new local declaration to locals. */
expr elaborator::push_local(type_context_old::tmp_locals & locals,
                            name const & n, expr const & type, binder_info binfo, expr const & /* ref */) {
#if 0 // TODO(Leo): the following check is too restrictive
    if (m_ctx.lctx().get_instance_fingerprint() &&
        m_ctx.is_class(type)) {
        throw elaborator_exception(ref, "invalid occurrence of local instance, it must be a declaration parameter");
    }
#endif
    return locals.push_local(n, type, binfo);
}

/* See method above */
expr elaborator::push_let(type_context_old::tmp_locals & locals,
                          name const & n, expr const & type, expr const & value, expr const & /* ref */) {
#if 0 // TODO(Leo): the following check is too restrictive
    if (m_ctx.lctx().get_instance_fingerprint() &&
        m_ctx.is_class(type)) {
        throw elaborator_exception(ref, "invalid occurrence of local instance, it must be a declaration parameter");
    }
#endif
    return locals.push_let(n, type, value);
}

expr elaborator::visit_lambda(expr const & e, optional<expr> const & expected_type) {
    type_context_old::tmp_locals locals(m_ctx);
    expr it  = e;
    expr ex;
    bool has_expected;
    if (expected_type) {
        ex = instantiate_mvars(*expected_type);
        has_expected = true;
    } else {
        has_expected = false;
    }
    while (is_lambda(it)) {
        if (has_expected) {
            ex = try_to_pi(ex);
            if (!is_pi(ex))
                has_expected = false;
        }
        expr d     = instantiate_rev_locals(binding_domain(it), locals);
        expr new_d = visit(d, none_expr());
        if (has_expected) {
            expr ex_d = binding_domain(ex);
            try_is_def_eq(new_d, ex_d);
        }
        expr ref_d = get_ref_for_child(binding_domain(it), it);
        new_d      = ensure_type(new_d, ref_d);
        expr l     = push_local(locals, binding_name(it), new_d, binding_info(it), ref_d);
        it = binding_body(it);
        if (has_expected) {
            lean_assert(is_pi(ex));
            ex = instantiate(binding_body(ex), l);
        }
    }
    expr b = instantiate_rev_locals(it, locals);
    expr new_b;
    if (has_expected) {
        new_b = visit(b, some_expr(ex));
    } else {
        new_b = visit(b, none_expr());
    }
    synthesize();
    expr r = locals.mk_lambda(new_b);
    return r;
}

expr elaborator::visit_pi(expr const & e) {
    type_context_old::tmp_locals locals(m_ctx);
    expr it  = e;
    expr parent_it = e;
    while (is_pi(it)) {
        expr d     = instantiate_rev_locals(binding_domain(it), locals);
        expr new_d = visit(d, none_expr());
        expr ref_d = get_ref_for_child(binding_domain(it), it);
        new_d      = ensure_type(new_d, ref_d);
        expr ref   = binding_domain(it);
        expr l     = push_local(locals, binding_name(it), new_d, binding_info(it), ref);
        parent_it  = it;
        it         = binding_body(it);
    }
    expr b     = instantiate_rev_locals(it, locals);
    expr new_b = visit(b, none_expr());
    expr ref_b = get_ref_for_child(it, parent_it);
    new_b      = ensure_type(new_b, ref_b);
    synthesize();
    expr r = locals.mk_pi(new_b);
    return r;
}

expr elaborator::visit_let(expr const & e, optional<expr> const & expected_type) {
    expr ref = e;
    expr new_type  = visit(let_type(e), none_expr());
    synthesize();
    expr new_value = visit(let_value(e), some_expr(new_type));
    expr ref_value = get_ref_for_child(let_value(e), ref);
    new_value      = enforce_type(new_value, new_type, "invalid let-expression", ref_value);
    synthesize();
    new_type       = instantiate_mvars(new_type);
    new_value      = instantiate_mvars(new_value);
    ensure_no_unassigned_metavars(new_value);
    type_context_old::tmp_locals locals(m_ctx);
    expr l = push_let(locals, let_name(e), new_type, new_value, ref);
    expr body      = instantiate_rev_locals(let_body(e), locals);
    expr new_body  = visit(body, expected_type);
    synthesize();
    expr new_e     = locals.mk_lambda(new_body);
    return new_e;
}

expr elaborator::visit_placeholder(expr const & e, optional<expr> const & expected_type) {
    expr const & ref = e;
    return mk_metavar(expected_type, ref);
}

static bool is_have_expr(expr const & e) {
    return is_app(e) && is_have_annotation(unwrap_pos(app_fn(e)));
}

expr elaborator::strict_visit(expr const & e, optional<expr> const & expected_type) {
    expr r = visit(e, expected_type);
    synthesize();
    r = instantiate_mvars(r);
    ensure_no_unassigned_metavars(r);
    return r;
}

expr elaborator::visit_have_expr(expr const & e, optional<expr> const & expected_type) {
    lean_assert(is_have_expr(e));
    expr lambda     = unwrap_pos(get_annotation_arg(app_fn(e)));
    expr type       = binding_domain(lambda);
    expr proof      = app_arg(e);
    expr new_type   = visit(type, none_expr());
    synthesize();
    new_type        = ensure_type(new_type, type);
    expr new_proof  = visit(proof, some_expr(new_type));
    new_proof       = enforce_type(new_proof, new_type, "invalid have-expression", proof);
    synthesize();
    ensure_no_unassigned_metavars(new_proof);
    type_context_old::tmp_locals locals(m_ctx);
    expr ref        = binding_domain(lambda);
    push_local(locals, binding_name(lambda), new_type, binding_info(lambda), ref);
    expr body       = instantiate_rev_locals(binding_body(lambda), locals);
    expr new_body   = visit(body, expected_type);
    expr new_lambda = locals.mk_lambda(new_body);
    return mk_app(mk_have_annotation(new_lambda), new_proof);
}

expr elaborator::visit_suffices_expr(expr const & e, optional<expr> const & expected_type) {
    lean_assert(is_suffices_annotation(e));
    expr body = get_annotation_arg(e);
    if (!is_app(body)) throw elaborator_exception(e, "ill-formed suffices expression");
    expr fn   = unwrap_pos(app_fn(body));
    expr rest = app_arg(body);
    if (!is_lambda(fn)) throw elaborator_exception(e, "ill-formed suffices expression");
    expr new_fn;
    expr type     = binding_domain(fn);
    expr new_type = visit(type, none_expr());
    synthesize();
    {
        type_context_old::tmp_locals locals(m_ctx);
        expr ref        = binding_domain(fn);
        push_local(locals, binding_name(fn), new_type, binding_info(fn), ref);
        expr body       = instantiate_rev_locals(binding_body(fn), locals);
        expr new_body   = visit(body, expected_type);
        synthesize();
        new_fn          = locals.mk_lambda(new_body);
    }
    expr new_rest = visit(rest, some_expr(new_type));
    new_rest      = enforce_type(new_rest, new_type, "invalid suffices-expression", rest);
    return mk_suffices_annotation(mk_app(new_fn, new_rest));
}

static expr mk_emptyc(expr const & src) {
    return copy_pos(src, mk_app(copy_pos(src, mk_constant(get_has_emptyc_emptyc_name())),
                                copy_pos(src, mk_expr_placeholder())));
}

expr elaborator::visit_emptyc_or_emptys(expr const & e, optional<expr> const & expected_type) {
    if (!expected_type) {
        return visit(mk_emptyc(e), expected_type);
    } else {
        synthesize_type_class_instances();
        expr new_expected_type = instantiate_mvars(*expected_type);
        if (is_optional_param(new_expected_type))
            new_expected_type = app_arg(app_fn(new_expected_type));
        expr S = get_app_fn(new_expected_type);
        if (is_constant(S) && is_structure(m_env, const_name(S))) {
            expr empty_struct = copy_pos(e, mk_structure_instance(name(), buffer<name>(), buffer<expr>()));
            return visit(empty_struct, expected_type);
        } else {
            return visit(mk_emptyc(e), expected_type);
        }
    }
}

expr elaborator::visit(expr const & e, optional<expr> const & expected_type) {
    flet<unsigned> inc_depth(m_depth, m_depth+1);
    trace_elab_detail(tout() << "[" << m_depth << "] visiting\n" << e << "\n";
                      if (expected_type) tout() << "expected type:\n" << instantiate_mvars(*expected_type) << "\n";);
    expr e2 = recover_expr_from_exception(expected_type, e, [&] () -> expr {
        if (auto p = get_pos(e)) {
            flet<optional<pos_info>> _(m_last_pos, p);
            return visit(unwrap_pos(e), expected_type);
        } else if (is_placeholder(e)) {
            return visit_placeholder(e, expected_type);
        } else if (is_typed_expr(e)) {
            return visit_typed_expr(e);
        } else if (is_have_expr(e)) {
            return copy_pos(e, visit_have_expr(e, expected_type));
        } else if (is_suffices_annotation(e)) {
            return copy_pos(e, visit_suffices_expr(e, expected_type));
        } else if (is_no_info(e)) {
            flet<bool> set(m_no_info, true);
            return visit(get_annotation_arg(e), expected_type);
        } else if (is_emptyc_or_emptys(e)) {
            return visit_emptyc_or_emptys(e, expected_type);
        } else if (is_sort_wo_universe(e)) {
            return visit(get_annotation_arg(e), expected_type);
        } else {
            switch (e.kind()) {
                case expr_kind::BVar: lean_unreachable();  // LCOV_EXCL_LINE
                case expr_kind::MVar:
                    return e;
                case expr_kind::Lit:
                    switch (lit_value(e).kind()) {
                    case literal_kind::Nat:
                        return visit_prenum(e, expected_type);
                    case literal_kind::String:
                        return e;
                    }
                    lean_unreachable();
                case expr_kind::MData:
                    return visit_mdata(e, expected_type, false);
                case expr_kind::Proj:
                    throw elaborator_exception(e, "unexpected occurrence of proj constructor");
                case expr_kind::Sort:
                    return visit_sort(e);
                case expr_kind::FVar:
                    return visit_local(e, expected_type);
                case expr_kind::Const:
                    return visit_constant(e, expected_type);
                case expr_kind::Lambda:
                    return visit_lambda(e, expected_type);
                case expr_kind::Pi:
                    return visit_pi(e);
                case expr_kind::App:
                    return visit_app(e, expected_type);
                case expr_kind::Let:
                    return visit_let(e, expected_type);
            }
            lean_unreachable(); // LCOV_EXCL_LINE
        }
    });
    return e2;
}

expr elaborator::get_default_numeral_type() {
    // TODO(Leo): allow user to modify default?
    return mk_constant(get_nat_name());
}

void elaborator::synthesize_numeral_types() {
    for (expr const & A : m_numeral_types) {
        trace_elab_numeral(tout() << "synthesize num type: " << A << ", " << instantiate_mvars(A) << "\n";);
        if (is_metavar_app(instantiate_mvars(A))) {
            if (!is_def_eq(A, get_default_numeral_type()))
                report_or_throw(elaborator_exception(A, "invalid numeral, failed to force numeral to be a nat"));
        }
    }
    m_numeral_types = list<expr>();
}

bool elaborator::synthesize_type_class_instance_core(expr const & mvar, expr const & inferred_inst, expr const & inst_type) {
    if (!ready_to_synthesize(inst_type))
        return false;
    metavar_decl mdecl = m_ctx.mctx().get_metavar_decl(mvar);
    expr ref = mvar;
    expr synthesized_inst = mk_instance_core(mdecl.get_context(), inst_type, ref);
    trace_elab_instances(tout() << inferred_inst << " =?= " << synthesized_inst << "\n";);
    if (!m_ctx.is_def_eq_at(mdecl.get_context(), inferred_inst, synthesized_inst)) {
        auto pp_fn = mk_pp_ctx();
        throw elaborator_exception(mvar,
                                   format("synthesized type class instance is not definitionally equal to expression "
                                          "inferred by typing rules, synthesized")
                                   + pp_indent(pp_fn, synthesized_inst)
                                   + line() + format("inferred")
                                   + pp_indent(pp_fn, inferred_inst)).ignore_if(has_synth_sorry(synthesized_inst));
    }
    return true;
}

bool elaborator::try_synthesize_type_class_instance(expr const & mvar) {
    expr inst = instantiate_mvars(mvar);
    expr inst_type = instantiate_mvars(infer_type(inst));
    return synthesize_type_class_instance_core(mvar, inst, inst_type);
}

void elaborator::synthesize_type_class_instances_step() {
    buffer<expr> to_keep;
    buffer<std::tuple<expr, expr, expr>> to_process;
    for (expr const & mvar : m_instances) {
        expr inst      = instantiate_mvars(mvar);
        expr inst_type = instantiate_mvars(infer_type(inst));
        if (!ready_to_synthesize(inst_type)) {
            to_keep.push_back(mvar);
        } else {
            to_process.emplace_back(mvar, inst, inst_type);
        }
    }
    if (to_process.empty())
        return;
    expr mvar, inst, inst_type;
    for (std::tuple<expr, expr, expr> const & curr : to_process) {
        std::tie(mvar, inst, inst_type) = curr;
        synthesize_type_class_instance_core(mvar, inst, inst_type);
    }
    m_instances = to_list(to_keep);
}

void elaborator::synthesize_type_class_instances() {
    while (true) {
        auto old_instances = m_instances;
        synthesize_type_class_instances_step();
        if (is_eqp(old_instances, m_instances))
            return;
    }
}

tactic_state elaborator::mk_tactic_state_for(expr const & mvar) {
    metavar_context mctx = m_ctx.mctx();
    metavar_decl mdecl   = mctx.get_metavar_decl(mvar);
    local_context lctx   = mdecl.get_context().instantiate_mvars(mctx);
    lctx                 = erase_inaccessible_annotations(lctx);
    expr type            = mctx.instantiate_mvars(mdecl.get_type());
    type                 = erase_inaccessible_annotations(type);
    m_ctx.set_mctx(mctx);
    return ::lean::mk_tactic_state_for(m_env, m_opts, m_decl_name, mctx, lctx, type);
}

void elaborator::synthesize() {
    synthesize_numeral_types();
    synthesize_type_class_instances();
}

void elaborator::ensure_no_unassigned_metavars_core(expr & e) {
    e = instantiate_mvars(e);
    if (!has_expr_metavar(e))
        return;
    name_set unassigned;
    for_each(e, [&](expr const & e, unsigned) {
        if (is_metavar_decl_ref(e))
            unassigned.insert(mvar_name(e));
        return true;
    });

    for (auto p : m_user_mvars) {
        auto const & m = p.first;
        auto const & info = p.second;
        auto m2 = get_app_fn(instantiate_mvars(m));
        // `m2` is still `m`, or it looks like a helper mvar introduced by the type_context_old
        if (is_mvar(m2) && unassigned.contains(mvar_name(m2))) {
            tactic_state s = mk_tactic_state_for(m);
            if (m_recover_from_errors) {
                // report error at `m`, but put `sorry` in `m2`
                if (!has_synth_sorry(infer_type(m))) {
                    auto tc = std::make_shared<type_context_old>(m_env, m_opts, m_ctx.mctx(), m_ctx.lctx());
                    message_builder out(tc, m_env, get_global_ios(), get_pos_info_provider()->get_file_name(),
                                        info.m_pos, ERROR);
                    out << "don't know how to synthesize placeholder";
                    if (info.m_context)
                        out << pp_until_meta_visible(mk_fmt_ctx(), *info.m_context);
                    out << "\n";
                    out << "context:\n" << mk_pair(s.pp(), m_opts);
                    out.report();
                    m_has_errors = true;
                }
                m_ctx.assign(m2, mk_sorry(infer_type(m2)));
            } else {
                throw failed_to_synthesize_placeholder_exception(m, s);
            }
        }
    }
    e = instantiate_mvars(e);
}

void elaborator::ensure_no_unassigned_metavars(expr & e) {
    ensure_no_unassigned_metavars_core(e);
    /* If we still have an unassigned mvar, it means the mvar doesn't directly correspond to a user-facing mvar.
     * That's bad (because we can't generate a sensible error message) and shouldn't happen. */
    lean_assert(has_synth_sorry(e) || !has_expr_metavar(e));
}

elaborator::snapshot::snapshot(elaborator const & e) {
    m_saved_mctx               = e.m_ctx.mctx();
    m_saved_instances          = e.m_instances;
    m_saved_numeral_types      = e.m_numeral_types;
}

void elaborator::snapshot::restore(elaborator & e) {
    e.m_ctx.set_mctx(m_saved_mctx);
    e.m_instances          = m_saved_instances;
    e.m_numeral_types      = m_saved_numeral_types;
}

/**
    \brief Auxiliary class for creating nice names for universe
    parameters introduced by the elaborator.

    This class also transforms remaining universe metavariables
    into parameters */
struct sanitize_param_names_fn : public replace_visitor {
    type_context_old &  m_ctx;
    name            m_p{"u"};
    name_set        m_L; /* All parameter names in the input expression. */
    unsigned        m_idx{1};
    buffer<name> &  m_new_param_names;
    /* If m_fixed == true, then we do not introduce new parameters.
       Remark: we should be able to infer the set of universe variables using just the
       theorem type. */
    bool            m_fixed;

    sanitize_param_names_fn(type_context_old & ctx, buffer<name> & new_lp_names):
        m_ctx(ctx), m_new_param_names(new_lp_names), m_fixed(false) {}

    sanitize_param_names_fn(type_context_old & ctx, elaborator::theorem_finalization_info const & info,
                            buffer<name> & new_lp_names):
        m_ctx(ctx), m_L(info.m_L),
        m_new_param_names(new_lp_names), m_fixed(true) {}

    level mk_param() {
        while (true) {
            name new_n = m_p.append_after(m_idx);
            m_idx++;
            if (!m_L.contains(new_n)) {
                m_new_param_names.push_back(new_n);
                return mk_univ_param(new_n);
            }
        }
    }

    level sanitize(level const & l) {
        name p("u");
        return replace(l, [&](level const & l) -> optional<level> {
                if (is_mvar(l)) {
                    if (is_metavar_decl_ref(l) && m_ctx.is_assigned(l)) {
                        return some_level(sanitize(*m_ctx.get_assignment(l)));
                    } else {
                        auto p = m_fixed ? mk_level_zero() : mk_param();
                        m_ctx.assign(l, p);
                        return some_level(p);
                    }
                }
                return none_level();
            });
    }

    virtual expr visit_constant(expr const & e) override {
        return update_constant(e, map(const_levels(e), [&](level const & l) { return sanitize(l); }));
    }

    virtual expr visit_sort(expr const & e) override {
        return update_sort(e, sanitize(sort_level(e)));
    }

    void collect_params(expr const & e) {
        m_L = collect_univ_params(e, m_L);
    }

    void collect_local_context_params() {
        m_ctx.lctx().for_each([&](local_decl const & l) {
                collect_params(m_ctx.instantiate_mvars(l.get_type()));
                if (auto v = l.get_value())
                    collect_params(m_ctx.instantiate_mvars(*v));
            });
    }

    expr sanitize(expr const & e) {
        expr r = operator()(e);
        return r;
    }

    elaborator::theorem_finalization_info mk_info() {
        return {m_L};
    }
};

/** When the output of the elaborator may contain meta-variables, we convert the type_context_old level meta-variables
    into regular kernel meta-variables. */
static expr replace_with_simple_metavars(metavar_context mctx, name_map<expr> & cache, expr const & e) {
    if (!has_expr_metavar(e)) return e;
    return replace(e, [&](expr const & e, unsigned) {
            if (is_metavar(e)) {
                if (auto r = cache.find(mvar_name(e))) {
                    return some_expr(*r);
                } else if (auto decl = mctx.find_metavar_decl(e)) {
                    expr new_type = replace_with_simple_metavars(mctx, cache, mctx.instantiate_mvars(decl->get_type()));
                    expr new_mvar = mk_metavar(mvar_name(e), new_type);
                    cache.insert(mvar_name(e), new_mvar);
                    return some_expr(new_mvar);
                } else if (is_metavar_decl_ref(e)) {
                    throw exception("unexpected occurrence of internal elaboration metavariable");
                } else {
                    return none_expr();
                }
            } else {
                return none_expr();
            }
        });
}

expr elaborator::elaborate(expr const & e) {
    expr r = visit(e,  none_expr());
    trace_elab_detail(tout() << "result before final checkpoint\n" << r << "\n";);
    synthesize();
    return r;
}

expr elaborator::elaborate_type(expr const & e) {
    expr const & ref = e;
    expr new_e = ensure_type(visit(e, none_expr()), ref);
    synthesize();
    return new_e;
}

expr_pair elaborator::elaborate_with_type(expr const & e, expr const & e_type) {
    expr const & ref = e;
    expr new_e, new_e_type;
    {
        expr Type  = visit(copy_pos(e_type, mk_sort(mk_level_placeholder())), none_expr());
        new_e_type = visit(e_type, some_expr(Type));
        new_e_type = ensure_type(new_e_type, e_type);
        new_e      = visit(e,      some_expr(new_e_type));
        synthesize();
    }
    new_e = enforce_type(new_e, new_e_type, "type mismatch", ref);
    return mk_pair(new_e, new_e_type);
}

void elaborator::finalize_core(sanitize_param_names_fn & S, buffer<expr> & es,
                               bool check_unassigned, bool to_simple_metavar, bool collect_local_ctx) {
    name_map<expr> to_simple_mvar_cache;
    for (expr & e : es) {
        e = instantiate_mvars(e);
        if (check_unassigned)
            ensure_no_unassigned_metavars(e);
        if (!check_unassigned && to_simple_metavar) {
            e = replace_with_simple_metavars(m_ctx.mctx(), to_simple_mvar_cache, e);
        }
        e = instantiate_mvars(e);
        S.collect_params(e);
    }
    if (collect_local_ctx)
        S.collect_local_context_params();
    for (expr & e : es) {
        e = S.sanitize(e);
    }
}

void elaborator::finalize(buffer<expr> & es, buffer<name> & new_lp_names, bool check_unassigned, bool to_simple_metavar) {
    sanitize_param_names_fn S(m_ctx, new_lp_names);
    finalize_core(S, es, check_unassigned, to_simple_metavar, true);
}

pair<expr, names> elaborator::finalize(expr const & e, bool check_unassigned, bool to_simple_metavar) {
    buffer<expr> es; es.push_back(e);
    buffer<name> new_lp_names;
    finalize(es, new_lp_names, check_unassigned, to_simple_metavar);
    return mk_pair(es[0], names(new_lp_names));
}

auto elaborator::finalize_theorem_type(expr const & type, buffer<name> & new_lp_names)
    -> pair<expr, theorem_finalization_info> {
    sanitize_param_names_fn S(m_ctx, new_lp_names);
    buffer<expr> es; es.push_back(type);
    bool check_unassigned  = true;
    bool to_simple_metavar = false;
    bool collect_local_ctx = true;
    finalize_core(S, es, check_unassigned, to_simple_metavar, collect_local_ctx);
    return mk_pair(es[0], S.mk_info());
}

expr elaborator::finalize_theorem_proof(expr const & val, theorem_finalization_info const & info) {
    buffer<name> dummy;
    sanitize_param_names_fn S(m_ctx, info, dummy);
    buffer<expr> es; es.push_back(val);
    bool check_unassigned  = true;
    bool to_simple_metavar = false;
    bool collect_local_ctx = false;
    finalize_core(S, es, check_unassigned, to_simple_metavar, collect_local_ctx);
    return es[0];
}

pair<expr, names>
elaborate(environment & env, options const & opts, name const & decl_name,
          metavar_context & mctx, local_context const & lctx, expr const & e,
          bool check_unassigned, bool recover_from_errors) {
    elaborator elab(env, opts, decl_name, mctx, lctx, recover_from_errors);
    expr r = elab.elaborate(e);
    auto p = elab.finalize(r, check_unassigned, true);
    mctx = elab.mctx();
    env  = elab.env();
    return p;
}

void initialize_elaborator() {
    g_elab_strategy = new name("elab_strategy");
    register_trace_class("elaborator");
    register_trace_class(name({"elaborator", "numeral"}));
    register_trace_class(name({"elaborator", "instances"}));
    register_trace_class(name({"elaborator", "equation"}));
    register_trace_class("elaborator_detail");
    register_trace_class("elaborator_debug");

    char const * elab_simple             = "elabSimple";
    char const * elab_with_expected_type = "elabWithExpectedType";
    char const * elab_as_eliminator      = "elabAsEliminator";

    register_system_attribute(
        elaborator_strategy_attribute(
            *g_elab_strategy,
            "internal attribute for the elaborator strategy for a given constant"));

    register_system_attribute(
        elaborator_strategy_proxy_attribute(
            elab_with_expected_type,
            "instructs elaborator that the arguments of the function application (f ...) "
            "should be elaborated using information about the expected type",
            elaborator_strategy::WithExpectedType));

    register_system_attribute(
        elaborator_strategy_proxy_attribute(
            elab_as_eliminator,
            "instructs elaborator that the arguments of the function application (f ...) "
            "should be elaborated as f were an eliminator",
            elaborator_strategy::AsEliminator));

    register_system_attribute(
        elaborator_strategy_proxy_attribute(
            elab_simple,
            "instructs elaborator that the arguments of the function application (f ...) "
            "should be elaborated from left to right, and without propagating information from the expected type "
            "to its arguments",
            elaborator_strategy::Simple));

    register_incompatible(elab_simple, elab_with_expected_type);
    register_incompatible(elab_simple, elab_as_eliminator);
    register_incompatible(elab_with_expected_type, elab_as_eliminator);

    g_elaborator_coercions          = new name{"elaborator", "coercions"};
    register_bool_option(*g_elaborator_coercions, LEAN_DEFAULT_ELABORATOR_COERCIONS,
                         "(elaborator) if true, the elaborator will automatically introduce coercions");
}

void finalize_elaborator() {
    delete g_elab_strategy;
    delete g_elaborator_coercions;
}
}
