lean2/src/library/tactic/inversion_tactic.cpp
2015-05-18 09:16:07 -07:00

1125 lines
50 KiB
C++

/*
Copyright (c) 2014 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: Leonardo de Moura
*/
#include <utility>
#include "util/sstream.h"
#include "kernel/abstract.h"
#include "kernel/instantiate.h"
#include "kernel/inductive/inductive.h"
#include "kernel/error_msgs.h"
#include "library/io_state_stream.h"
#include "library/locals.h"
#include "library/util.h"
#include "library/constants.h"
#include "library/reducible.h"
#include "library/tactic/tactic.h"
#include "library/tactic/expr_to_tactic.h"
#include "library/tactic/class_instance_synth.h"
#include "library/tactic/inversion_tactic.h"
#include "library/tactic/clear_tactic.h"
namespace lean {
namespace inversion {
result::result(list<goal> const & gs, list<list<expr>> const & args, list<implementation_list> const & imps,
list<rename_map> const & rs, name_generator const & ngen, substitution const & subst):
m_goals(gs), m_args(args), m_implementation_lists(imps),
m_renames(rs), m_ngen(ngen), m_subst(subst) {
lean_assert_eq(length(m_goals), length(m_args));
lean_assert_eq(length(m_goals), length(m_implementation_lists));
lean_assert_eq(length(m_goals), length(m_renames));
}
}
/**
\brief Given eq_rec of the form <tt>@eq.rec.{l₂ l₁} A a (λ (a' : A) (h : a = a'), B a') b a p</tt>,
apply the eq_rec_eq definition to produce the equality
b = @eq.rec.{l₂ l₁} A a (λ (a' : A) (h : a = a'), B a') b a p
The eq_rec_eq definition is of the form
definition eq_rec_eq.{l₁ l₂} {A : Type.{l₁}} {B : A → Type.{l₂}} [h : is_hset A] {a : A} (b : B a) (p : a = a) :
b = @eq.rec.{l₂ l₁} A a (λ (a' : A) (h : a = a'), B a') b a p :=
...
*/
optional<expr> apply_eq_rec_eq(type_checker & tc, io_state const & ios, list<expr> const & ctx, expr const & eq_rec) {
buffer<expr> args;
expr const & eq_rec_fn = get_app_args(eq_rec, args);
if (args.size() != 6)
return none_expr();
expr const & p = args[5];
if (!is_local(p) || !is_eq_a_a(tc, mlocal_type(p)))
return none_expr();
expr const & A = args[0];
auto is_hset_A = mk_hset_instance(tc, ios, ctx, A);
if (!is_hset_A)
return none_expr();
levels ls = const_levels(eq_rec_fn);
level l2 = head(ls);
level l1 = head(tail(ls));
expr C = tc.whnf(args[2]).first;
if (!is_lambda(C))
return none_expr();
expr a1 = mk_local(tc.mk_fresh_name(), binding_domain(C));
C = tc.whnf(instantiate(binding_body(C), a1)).first;
if (!is_lambda(C))
return none_expr();
C = binding_body(C);
if (!closed(C))
return none_expr();
expr B = Fun(a1, C);
expr a = args[1];
expr b = args[3];
expr r = mk_constant(get_eq_rec_eq_name(), {l1, l2});
return some_expr(mk_app({r, A, B, *is_hset_A, a, b, p}));
}
typedef inversion::implementation_ptr implementation_ptr;
typedef inversion::implementation_list implementation_list;
static void replace(implementation_list const & imps, unsigned sz, expr const * from, expr const * to) {
lean_assert(std::all_of(from, from+sz, is_local));
for (implementation_ptr const & imp : imps) {
imp->update_exprs([&](expr const & e) {
return instantiate_rev(abstract_locals(e, sz, from), sz, to);
});
}
}
static void replace(implementation_list const & imps, buffer<expr> const & from, buffer<expr> const & to) {
lean_assert(from.size() == to.size());
return replace(imps, from.size(), from.data(), to.data());
}
static void replace(implementation_list const & imps, expr const & from, expr const & to) {
return replace(imps, 1, &from, &to);
}
class inversion_tac {
environment const & m_env;
io_state const & m_ios;
type_checker & m_tc;
list<name> m_ids;
name_generator m_ngen;
substitution m_subst;
bool m_dep_elim;
bool m_proof_irrel;
unsigned m_nparams;
unsigned m_nindices;
unsigned m_nminors;
declaration m_I_decl;
declaration m_cases_on_decl;
bool m_throw_tactic_exception; // if true, then throw tactic_exception in case of failure, instead of returning none
bool m_clear_elim; // if true, then clear eliminated variables
expr whnf(expr const & e) { return m_tc.whnf(e).first; }
expr infer_type(expr const & e) { return m_tc.infer(e).first; }
void init_inductive_info(name const & n) {
m_dep_elim = inductive::has_dep_elim(m_env, n);
m_nindices = *inductive::get_num_indices(m_env, n);
m_nparams = *inductive::get_num_params(m_env, n);
// This tactic is bases on cases_on construction which only has
// minor premises for the introduction rules of this datatype.
// For non-mutually recursive datatypes inductive::get_num_intro_rules == inductive::get_num_minor_premises
m_nminors = *inductive::get_num_intro_rules(m_env, n);
m_I_decl = m_env.get(n);
m_cases_on_decl = m_env.get({n, "cases_on"});
}
bool is_inversion_applicable(expr const & t) {
buffer<expr> args;
expr const & fn = get_app_args(t, args);
if (!is_constant(fn))
return false;
if (!inductive::is_inductive_decl(m_env, const_name(fn)))
return false;
if (!m_env.find(name{const_name(fn), "cases_on"}) || !m_env.find(get_eq_name()))
return false;
if (m_proof_irrel && !m_env.find(get_heq_name()))
return false;
init_inductive_info(const_name(fn));
if (args.size() != m_nindices + m_nparams)
return false;
return true;
}
pair<expr, expr> mk_eq(expr const & lhs, expr const & rhs) {
expr lhs_type = infer_type(lhs);
expr rhs_type = infer_type(rhs);
level l = sort_level(m_tc.ensure_type(lhs_type).first);
constraint_seq cs;
if (m_tc.is_def_eq(lhs_type, rhs_type, justification(), cs) && !cs) {
return mk_pair(mk_app(mk_constant(get_eq_name(), to_list(l)), lhs_type, lhs, rhs),
mk_app(mk_constant(get_eq_refl_name(), to_list(l)), lhs_type, lhs));
} else {
return mk_pair(mk_app(mk_constant(get_heq_name(), to_list(l)), lhs_type, lhs, rhs_type, rhs),
mk_app(mk_constant(get_heq_refl_name(), to_list(l)), lhs_type, lhs));
}
}
void assign(goal const & g, expr const & val) {
::lean::assign(m_subst, g, val);
}
/** \brief We say h has independent indices IF
1- it is *not* an indexed inductive family, OR
2- it is an indexed inductive family, but all indices are distinct local constants,
and all hypotheses of g different from h and indices, do not depend on the indices.
3- if not m_dep_elim, then the conclusion does not depend on the indices.
*/
bool has_indep_indices(goal const & g, expr const & h, expr const & h_type) {
if (m_nindices == 0)
return true;
buffer<expr> args;
get_app_args(h_type, args);
lean_assert(m_nindices <= args.size());
unsigned fidx = args.size() - m_nindices;
for (unsigned i = fidx; i < args.size(); i++) {
if (!is_local(args[i]))
return false; // the indices must be local constants
for (unsigned j = 0; j < i; j++) {
if (is_local(args[j]) && mlocal_name(args[j]) == mlocal_name(args[i]))
return false; // the indices must be distinct local constants
}
}
if (!m_dep_elim) {
expr const & g_type = g.get_type();
if (depends_on(g_type, h))
return false;
}
buffer<expr> hyps;
g.get_hyps(hyps);
for (expr const & h1 : hyps) {
if (mlocal_name(h1) == mlocal_name(h))
continue;
if (std::any_of(args.end() - m_nindices, args.end(),
[&](expr const & arg) { return mlocal_name(arg) == mlocal_name(h1); }))
continue;
// h1 is not h nor any of the indices
// Thus, it must not depend on the indices
if (depends_on_any(h1, m_nindices, args.end() - m_nindices))
return false;
}
return true;
}
/** \brief Split hyps into two "telescopes".
- non_deps : hypotheses that do not depend on H
- deps : hypotheses that depend on H (directly or indirectly)
*/
void split_deps(buffer<expr> const & hyps, expr const & H, buffer<expr> & non_deps, buffer<expr> & deps,
bool clear_H = false) {
for (expr const & hyp : hyps) {
expr const & hyp_type = mlocal_type(hyp);
if (depends_on(hyp_type, H) || depends_on_any(hyp_type, deps)) {
deps.push_back(hyp);
} else if (hyp != H || !clear_H) {
non_deps.push_back(hyp);
}
}
}
/** \brief Given a goal of the form
Ctx, h : I A j, D |- T
where the type of h is the inductive datatype (I A j) where A are parameters,
and j the indices. Generate the goal
Ctx, h : I A j, D, j' : J, h' : I A j' |- j == j' -> h == h' -> T
Remark: (j == j' -> h == h') is a "telescopic" equality. If the environment
is proof irrelevant, it is built using homogenous and heterogeneous equalities.
If the environment is proof relevant, it is built using homogeneous equality
and the eq.rec (this construction is much more complex than the proof irrelevant
one).
Remark: j is sequence of terms, and j' a sequence of local constants.
The original goal is solved if we can solve the produced goal.
\remark h_type is mlocal_type(h) after normalization
*/
goal generalize_indices(goal const & g, expr const & h, expr const & h_type) {
buffer<expr> hyps;
g.get_hyps(hyps);
expr m = g.get_meta();
expr m_type = g.get_type();
name h_new_name = get_unused_name(local_pp_name(h), hyps);
buffer<expr> I_args;
expr const & I = get_app_args(h_type, I_args);
expr h_new_type = mk_app(I, I_args.size() - m_nindices, I_args.data());
expr d = whnf(infer_type(h_new_type));
name t_prefix("t");
unsigned nidx = 1;
if (m_proof_irrel) {
unsigned eq_idx = 1;
name eq_prefix("H");
buffer<expr> ts;
buffer<expr> eqs;
buffer<expr> refls;
auto add_eq = [&](expr const & lhs, expr const & rhs) {
pair<expr, expr> p = mk_eq(lhs, rhs);
expr new_eq = p.first;
expr new_refl = p.second;
eqs.push_back(mk_local(m_ngen.next(), g.get_unused_name(eq_prefix, eq_idx), new_eq, binder_info()));
refls.push_back(new_refl);
};
for (unsigned i = I_args.size() - m_nindices; i < I_args.size(); i++) {
expr t_type = binding_domain(d);
expr t = mk_local(m_ngen.next(), g.get_unused_name(t_prefix, nidx), t_type, binder_info());
expr const & index = I_args[i];
add_eq(index, t);
h_new_type = mk_app(h_new_type, t);
hyps.push_back(t);
ts.push_back(t);
d = instantiate(binding_body(d), t);
}
expr h_new = mk_local(m_ngen.next(), h_new_name, h_new_type, local_info(h));
if (m_dep_elim)
add_eq(h, h_new);
hyps.push_back(h_new);
expr new_type = Pi(eqs, g.get_type());
expr new_meta = mk_app(mk_metavar(m_ngen.next(), Pi(hyps, new_type)), hyps);
goal new_g(new_meta, new_type);
expr val = mk_app(mk_app(mk_app(Fun(ts, Fun(h_new, new_meta)), m_nindices, I_args.end() - m_nindices), h),
refls);
assign(g, val);
return new_g;
} else {
// proof relevant version
buffer<expr> ss;
buffer<expr> ts;
buffer<expr> refls;
for (unsigned i = I_args.size() - m_nindices; i < I_args.size(); i++) {
expr t_type = binding_domain(d);
expr t = mk_local(m_ngen.next(), g.get_unused_name(t_prefix, nidx), t_type, binder_info());
h_new_type = mk_app(h_new_type, t);
ss.push_back(I_args[i]);
refls.push_back(mk_refl(m_tc, I_args[i]));
hyps.push_back(t);
ts.push_back(t);
d = instantiate(binding_body(d), t);
}
expr h_new = mk_local(m_ngen.next(), h_new_name, h_new_type, local_info(h));
ts.push_back(h_new);
ss.push_back(h);
refls.push_back(mk_refl(m_tc, h));
hyps.push_back(h_new);
buffer<expr> eqs;
mk_telescopic_eq(m_tc, ss, ts, eqs);
ts.pop_back();
expr new_type = Pi(eqs, g.get_type());
expr new_meta = mk_app(mk_metavar(m_ngen.next(), Pi(hyps, new_type)), hyps);
goal new_g(new_meta, new_type);
expr val = mk_app(mk_app(mk_app(Fun(ts, Fun(h_new, new_meta)), m_nindices, I_args.end() - m_nindices), h),
refls);
assign(g, val);
return new_g;
}
}
/** \brief Generalize h dependencies.
This tactic uses this method only if has_indep_indices(hyps, h, h_type) returns true.
The hypotheses that depend on h are stored in deps.
*/
goal generalize_dependecies(goal const & g, expr const & h, buffer<expr> & deps) {
buffer<expr> hyps;
g.get_hyps(hyps);
hyps.erase_elem(h);
buffer<expr> non_deps;
split_deps(hyps, h, non_deps, deps);
buffer<expr> & new_hyps = non_deps;
new_hyps.push_back(h);
expr new_type = Pi(deps, g.get_type());
expr new_meta = mk_app(mk_metavar(m_ngen.next(), Pi(new_hyps, new_type)), new_hyps);
goal new_g(new_meta, new_type);
expr val = mk_app(new_meta, deps);
assign(g, val);
return new_g;
}
/**
\brief Given a goal of the form
Ctx, h : I A j |- T[h]
produce the subgoals
Ctx, h : I A j |- b_1 : B_1 -> T [C_1 A b_1]
...
Ctx, h : I A j |- b_n : B_n -> T [C_n A b_n]
where the C_i's are the constructors (aka introduction rules) of the inductive datatype h.
The hypothesis h must be the last hypothesis in the input goal. Consequently, no other
hypothesis depends on it.
The implementation list is external auxiliary data associated with constructors.
For example, we use it to compile recursive equations. Each implementation corresponds to one
equation. Each equation is tagged with a constructor C_i. The constructor is retrieved using
the virtual method get_constructor_name. We split the list imps in n lists. One for each
subgoal. An implementation associated with the constructor i is in the i-th resulting list.
\remark This method assumes the indices j are local constants, and only h and j may depend on j.
*/
std::pair<list<goal>, list<implementation_list>> apply_cases_on(goal const & g, implementation_list const & imps, bool has_indep_indices) {
buffer<expr> hyps;
g.get_hyps(hyps);
expr const & h = hyps.back();
expr const & h_type = whnf(mlocal_type(h));
buffer<expr> I_args;
expr const & I = get_app_args(h_type, I_args);
name const & I_name = const_name(I);
expr g_type = g.get_type();
expr cases_on;
level g_lvl = sort_level(m_tc.ensure_type(g_type).first);
if (m_cases_on_decl.get_num_univ_params() != m_I_decl.get_num_univ_params()) {
cases_on = mk_constant({I_name, "cases_on"}, cons(g_lvl, const_levels(I)));
} else {
if (!is_zero(g_lvl)) {
if (m_throw_tactic_exception)
throw tactic_exception(sstream() << "invalid 'cases' tactic, '" << const_name(I) << "' can only eliminate to Prop");
else
throw inversion_exception();
}
cases_on = mk_constant({I_name, "cases_on"}, const_levels(I));
}
// add params
cases_on = mk_app(cases_on, m_nparams, I_args.data());
// add type former
expr type_former = g_type;
if (m_dep_elim)
type_former = Fun(h, type_former);
type_former = Fun(m_nindices, I_args.end() - m_nindices, type_former);
cases_on = mk_app(cases_on, type_former);
// add indices
cases_on = mk_app(cases_on, m_nindices, I_args.end() - m_nindices);
// add h
cases_on = mk_app(cases_on, h);
buffer<name> intro_names;
get_intro_rule_names(m_env, I_name, intro_names);
lean_assert(m_nminors == intro_names.size());
buffer<expr> new_hyps;
if (has_indep_indices)
new_hyps.append(hyps.size() - 1, hyps.data());
else
new_hyps.append(hyps.size() - m_nindices - 1, hyps.data());
// add a subgoal for each minor premise of cases_on
expr cases_on_type = whnf(infer_type(cases_on));
buffer<goal> new_goals;
buffer<implementation_list> new_imps;
for (unsigned i = 0; i < m_nminors; i++) {
expr new_type = binding_domain(cases_on_type);
expr new_meta = mk_app(mk_metavar(m_ngen.next(), Pi(new_hyps, new_type)), new_hyps);
goal new_g(new_meta, new_type);
new_goals.push_back(new_g);
cases_on = mk_app(cases_on, new_meta);
cases_on_type = whnf(binding_body(cases_on_type)); // the minor premises do not depend on each other
name const & intro_name = intro_names[i];
new_imps.push_back(filter(imps, [&](implementation_ptr const & imp) { return imp->get_constructor_name() == intro_name; }));
}
assign(g, cases_on);
return mk_pair(to_list(new_goals), to_list(new_imps));
}
// Store in \c r the number of arguments for each cases_on minor.
void get_minors_nargs(buffer<unsigned> & r) {
expr cases_on_type = m_cases_on_decl.get_type();
for (unsigned i = 0; i < m_nparams + 1 + m_nindices + 1; i++)
cases_on_type = binding_body(cases_on_type);
for (unsigned i = 0; i < m_nminors; i++) {
expr minor_type = binding_domain(cases_on_type);
unsigned nargs = 0;
while (is_pi(minor_type)) {
nargs++;
minor_type = binding_body(minor_type);
}
r.push_back(nargs);
cases_on_type = binding_body(cases_on_type);
}
}
/**
The method apply_cases_on produces subgoals of the form
Ctx, h : I A j |- b_i : B_i -> T [C_i A b_i]
The b_i are the arguments of the constructor C_i.
This method moves the b_i's to the hypotheses list.
*/
std::pair<list<goal>, list<list<expr>>> intros_minors_args(list<goal> gs) {
buffer<unsigned> minors_nargs;
get_minors_nargs(minors_nargs);
lean_assert(length(gs) == minors_nargs.size());
buffer<goal> new_gs;
buffer<list<expr>> new_args;
for (unsigned i = 0; i < minors_nargs.size(); i++) {
goal const & g = head(gs);
unsigned nargs = minors_nargs[i];
buffer<expr> hyps;
g.get_hyps(hyps);
buffer<expr> new_hyps;
new_hyps.append(hyps);
expr g_type = g.get_type();
buffer<expr> curr_new_args;
for (unsigned i = 0; i < nargs; i++) {
expr type = binding_domain(g_type);
name new_h_name;
if (m_ids) {
new_h_name = head(m_ids);
m_ids = tail(m_ids);
} else {
new_h_name = binding_name(g_type);
}
expr new_h = mk_local(m_ngen.next(), get_unused_name(new_h_name, new_hyps), type, binder_info());
curr_new_args.push_back(new_h);
new_hyps.push_back(new_h);
g_type = instantiate(binding_body(g_type), new_h);
}
new_args.push_back(to_list(curr_new_args));
g_type = head_beta_reduce(g_type);
expr new_meta = mk_app(mk_metavar(m_ngen.next(), Pi(new_hyps, g_type)), new_hyps);
goal new_g(new_meta, g_type);
new_gs.push_back(new_g);
expr val = Fun(nargs, new_hyps.end() - nargs, new_meta);
assign(g, val);
gs = tail(gs);
}
return mk_pair(to_list(new_gs), to_list(new_args));
}
// auxiliary exception used to interrupt execution
struct inversion_exception {};
[[ noreturn ]] void throw_ill_formed_goal() {
if (m_throw_tactic_exception)
throw tactic_exception("invalid 'cases' tactic, ill-formed goal");
else
throw inversion_exception();
}
[[ noreturn ]] void throw_ill_typed_goal() {
if (m_throw_tactic_exception)
throw tactic_exception("invalid 'cases' tactic, ill-typed goal");
else
throw inversion_exception();
}
[[ noreturn ]] void throw_lift_down_failure() {
if (m_throw_tactic_exception)
throw tactic_exception("invalid 'cases' tactic, lift.down failed");
else
throw inversion_exception();
}
void throw_unification_eq_rec_failure(goal const & g, expr const & eq) {
if (m_throw_tactic_exception) {
throw tactic_exception([=](formatter const & fmt) {
format r("invalid 'cases' tactic, unification failed to eliminate eq.rec in the homogeneous equality");
r += pp_indent_expr(fmt, eq);
r += compose(line(), format("auxiliary goal at time of failure"));
r += nest(get_pp_indent(fmt.get_options()), compose(line(), g.pp(fmt)));
return r;
});
} else {
throw inversion_exception();
}
}
/** \brief Process goal of the form: Pi (H : eq.rec A s C a s p = b), R
The idea is to reduce it to
Pi (H : a = b), R
when A is a hset
and then invoke intro_next_eq recursively.
\remark \c type is the type of \c g after some normalization
*/
goal intro_next_eq_rec(goal const & g, expr const & type) {
lean_assert(is_pi(type));
buffer<expr> hyps;
g.get_hyps(hyps);
expr const & eq = binding_domain(type);
expr const & lhs = app_arg(app_fn(eq));
expr const & rhs = app_arg(eq);
lean_assert(is_eq_rec(lhs));
// lhs is of the form (eq.rec A s C a s p)
// aux_eq is a term of type ((eq.rec A s C a s p) = a)
auto aux_eq = apply_eq_rec_eq(m_tc, m_ios, to_list(hyps), lhs);
if (!aux_eq || has_expr_metavar_relaxed(*aux_eq)) {
throw_unification_eq_rec_failure(g, eq);
}
buffer<expr> lhs_args;
get_app_args(lhs, lhs_args);
expr const & reduced_lhs = lhs_args[3];
expr new_eq = ::lean::mk_eq(m_tc, reduced_lhs, rhs);
expr new_type = update_binding(type, new_eq, binding_body(type));
expr new_meta = mk_app(mk_metavar(m_ngen.next(), Pi(hyps, new_type)), hyps);
goal new_g(new_meta, new_type);
// create assignment for g
expr A = infer_type(lhs);
level lvl = sort_level(m_tc.ensure_type(A).first);
// old_eq : eq.rec A s C a s p = b
expr old_eq = mk_local(m_ngen.next(), binding_name(type), eq, binder_info());
// aux_eq : a = eq.rec A s C a s p
expr trans_eq = mk_app({mk_constant(get_eq_trans_name(), {lvl}), A, reduced_lhs, lhs, rhs, *aux_eq, old_eq});
// trans_eq : a = b
expr val = Fun(old_eq, mk_app(new_meta, trans_eq));
assign(g, val);
return intro_next_eq(new_g);
}
/**
\brief Process goal of the form: Ctx |- Pi (H : a == b), R when a and b have the same type
The idea is to reduce it to
Ctx, H : a = b |- R
This method is only used if the environment has a proof irrelevant Prop.
*/
goal intro_next_heq(goal const & g) {
lean_assert(m_proof_irrel);
expr const & type = g.get_type();
expr eq = binding_domain(type);
lean_assert(const_name(get_app_fn(eq)) == get_heq_name());
buffer<expr> args;
expr const & heq_fn = get_app_args(eq, args);
constraint_seq cs;
if (m_tc.is_def_eq(args[0], args[2], justification(), cs) && !cs) {
buffer<expr> hyps;
g.get_hyps(hyps);
expr new_eq = mk_app(mk_constant(get_eq_name(), const_levels(heq_fn)), args[0], args[1], args[3]);
expr new_hyp = mk_local(m_ngen.next(), g.get_unused_name(binding_name(type)), new_eq, binder_info());
expr new_type = instantiate(binding_body(type), new_hyp);
hyps.push_back(new_hyp);
expr new_mvar = mk_metavar(m_ngen.next(), Pi(hyps, new_type));
expr new_meta = mk_app(new_mvar, hyps);
goal new_g(new_meta, new_type);
hyps.pop_back();
expr H = mk_local(m_ngen.next(), g.get_unused_name(binding_name(type)), binding_domain(type), binder_info());
expr to_eq = mk_app(mk_constant(get_heq_to_eq_name(), const_levels(heq_fn)), args[0], args[1], args[3], H);
expr val = Fun(H, mk_app(mk_app(new_mvar, hyps), to_eq));
assign(g, val);
return new_g;
} else {
if (m_throw_tactic_exception) {
throw tactic_exception("invalid 'cases' tactic, unification failed to reduce heterogeneous equality into homogeneous one");
} else {
throw inversion_exception();
}
}
}
/** \brief Process goal of the form: Ctx |- Pi (H : a = b), R
The idea is to reduce it to
Ctx, H : a = b |- R
\remark \c type is the type of \c g after some normalization
*/
goal intro_next_eq_simple(goal const & g, expr const & type) {
expr eq = binding_domain(type);
lean_assert(const_name(get_app_fn(eq)) == get_eq_name());
buffer<expr> hyps;
g.get_hyps(hyps);
expr new_hyp = mk_local(m_ngen.next(), g.get_unused_name(binding_name(type)), binding_domain(type), binder_info());
expr new_type = instantiate(binding_body(type), new_hyp);
hyps.push_back(new_hyp);
expr new_meta = mk_app(mk_metavar(m_ngen.next(), Pi(hyps, new_type)), hyps);
goal new_g(new_meta, new_type);
expr val = Fun(new_hyp, new_meta);
assign(g, val);
return new_g;
}
goal intro_next_eq(goal const & g) {
expr type = g.get_type();
if (!is_pi(type))
throw_ill_formed_goal();
expr eq = binding_domain(type);
expr const & eq_fn = get_app_fn(eq);
if (!is_constant(eq_fn))
throw_ill_formed_goal();
if (const_name(eq_fn) == get_eq_name()) {
expr const & lhs = app_arg(app_fn(eq));
expr const & rhs = app_arg(eq);
expr new_lhs = whnf(lhs);
expr new_rhs = whnf(rhs);
if (lhs != new_lhs || rhs != new_rhs) {
eq = mk_app(app_fn(app_fn(eq)), new_lhs, new_rhs);
type = update_binding(type, eq, binding_body(type));
}
if (!m_proof_irrel && is_eq_rec(new_lhs)) {
return intro_next_eq_rec(g, type);
} else {
return intro_next_eq_simple(g, type);
}
} else if (m_proof_irrel && const_name(eq_fn) == get_heq_name()) {
return intro_next_heq(g);
} else {
throw_ill_formed_goal();
}
}
/**
\brief The no_confusion constructions uses lifts in the proof relevant version.
We must apply lift.down to eliminate the auxiliary lift.
*/
expr lift_down(expr const & v) {
if (auto r = lift_down_if_hott(m_tc, v))
return *r;
else
throw_lift_down_failure();
}
buffer<expr> m_c_args; // current intro/constructor args that may be renamed by unify_eqs
rename_map m_renames;
implementation_list m_imps;
void store_rename(expr const & old_hyp, expr const & new_hyp) {
for (expr & c_arg : m_c_args) {
if (is_local(c_arg) && mlocal_name(old_hyp) == mlocal_name(c_arg))
c_arg = new_hyp;
}
if (is_local(new_hyp))
m_renames.insert(mlocal_name(old_hyp), mlocal_name(new_hyp));
}
/** \brief Update m_renames with old_hyps --> new_hyps. */
void store_renames(buffer<expr> const & old_hyps, buffer<expr> const & new_hyps) {
lean_assert(old_hyps.size() == new_hyps.size());
for (unsigned i = 0; i < old_hyps.size(); i++) {
store_rename(old_hyps[i], new_hyps[i]);
}
}
[[ noreturn ]] void throw_unify_eqs_failure(goal const & g) {
if (m_throw_tactic_exception) {
throw tactic_exception([=](formatter const & fmt) {
format r("invalid 'cases' tactic, unification failed");
r += compose(line(), format("auxiliary goal at time of failure"));
r += nest(get_pp_indent(fmt.get_options()), compose(line(), g.pp(fmt)));
return r;
});
} else {
throw inversion_exception();
}
}
// Remark: it also updates m_renames and m_imps
optional<goal> unify_eqs(goal g, unsigned neqs) {
if (neqs == 0)
return optional<goal>(g); // done
g = intro_next_eq(g);
buffer<expr> hyps;
g.get_hyps(hyps);
lean_assert(!hyps.empty());
expr Heq = hyps.back();
buffer<expr> Heq_args;
get_app_args(mlocal_type(Heq), Heq_args);
expr const & A = whnf(Heq_args[0]);
expr lhs = whnf(Heq_args[1]);
expr rhs = whnf(Heq_args[2]);
constraint_seq cs;
if (m_proof_irrel && m_tc.is_def_eq(lhs, rhs, justification(), cs) && !cs) {
// deletion transition: t == t
hyps.pop_back(); // remove t == t equality
expr new_type = g.get_type();
expr new_meta = mk_app(mk_metavar(m_ngen.next(), Pi(hyps, new_type)), hyps);
goal new_g(new_meta, new_type);
assign(g, new_meta);
return unify_eqs(new_g, neqs-1);
}
buffer<expr> lhs_args, rhs_args;
expr const & lhs_fn = get_app_args(lhs, lhs_args);
expr const & rhs_fn = get_app_args(rhs, rhs_args);
expr const & g_type = g.get_type();
level const & g_lvl = sort_level(m_tc.ensure_type(g_type).first);
if (is_constant(lhs_fn) &&
is_constant(rhs_fn) &&
inductive::is_intro_rule(m_env, const_name(lhs_fn)) &&
inductive::is_intro_rule(m_env, const_name(rhs_fn))) {
buffer<expr> A_args;
expr const & A_fn = get_app_args(A, A_args);
if (!is_constant(A_fn) || !inductive::is_inductive_decl(m_env, const_name(A_fn)))
throw_ill_typed_goal();
name no_confusion_name(const_name(A_fn), "no_confusion");
if (!m_env.find(no_confusion_name)) {
if (m_throw_tactic_exception)
throw tactic_exception(sstream() << "invalid 'cases' tactic, construction '" << no_confusion_name << "' is not available in the environment");
else
throw inversion_exception();
}
expr no_confusion = mk_app(mk_app(mk_constant(no_confusion_name, cons(g_lvl, const_levels(A_fn))), A_args), g_type, lhs, rhs, Heq);
if (const_name(lhs_fn) == const_name(rhs_fn)) {
// injectivity transition
expr new_type = binding_domain(whnf(infer_type(no_confusion)));
if (m_proof_irrel || !depends_on(g_type, hyps.back()))
hyps.pop_back(); // remove processed equality
expr new_mvar = mk_metavar(m_ngen.next(), Pi(hyps, new_type));
expr new_meta = mk_app(new_mvar, hyps);
goal new_g(new_meta, new_type);
expr val = lift_down(mk_app(no_confusion, new_meta));
assign(g, val);
unsigned A_nparams = *inductive::get_num_params(m_env, const_name(A_fn));
lean_assert(lhs_args.size() >= A_nparams);
return unify_eqs(new_g, neqs - 1 + lhs_args.size() - A_nparams);
} else {
// conflict transition, Heq is of the form c_1 ... = c_2 ..., where c_1 and c_2 are different constructors/intro rules.
expr val = lift_down(no_confusion);
assign(g, val);
return optional<goal>(); // goal has been solved
}
}
if (is_local(rhs)) {
// solution transition, Heq is of the form t = y, where y is a local constant
// assume the current goal is of the form
//
// non_deps, deps[rhs], H : lhs = rhs |- C[rhs]
//
// We use non_deps to denote hypotheses that do not depend on rhs,
// and deps[rhs] to denote hypotheses that depend on it.
//
// The resultant goal is of the form
//
// non_deps, deps[lhs] |- C[lhs]
//
// Now, assume ?m1 is a solution for the resultant goal.
// Then,
//
// @eq.rec A lhs (fun rhs, Pi(deps[rhs], C[rhs])) (?m1 non_deps) rhs H deps[rhs]
//
// is a solution for the original goal.
// Remark: A is the type of lhs and rhs
hyps.pop_back(); // remove processed equality
buffer<expr> non_deps, deps;
bool clear_rhs = m_clear_elim;
split_deps(hyps, rhs, non_deps, deps, clear_rhs);
if (deps.empty() && !depends_on(g_type, rhs)) {
// eq.rec is not necessary
buffer<expr> & new_hyps = non_deps;
expr new_type = g_type;
expr new_mvar = mk_metavar(m_ngen.next(), Pi(new_hyps, new_type));
expr new_meta = mk_app(new_mvar, new_hyps);
goal new_g(new_meta, new_type);
assign(g, new_meta);
return unify_eqs(new_g, neqs-1);
} else {
expr deps_g_type = Pi(deps, g_type);
level eq_rec_lvl1 = sort_level(m_tc.ensure_type(deps_g_type).first);
level eq_rec_lvl2 = sort_level(m_tc.ensure_type(A).first);
expr tformer;
if (m_proof_irrel) {
tformer = Fun(rhs, deps_g_type);
} else {
if (depends_on(lhs, rhs)) {
// tformer must be of the form
// fun x, fun (Heq : lhs = x, ...)
// If the lhs contains occurrences of rhs, then
// we would produce the following ill-typed tformer
// fun x, fun (Heq : lhs[x] = x, ...)
throw_unify_eqs_failure(g);
}
tformer = Fun(rhs, Fun(Heq, deps_g_type));
}
expr eq_rec = mk_constant(get_eq_rec_name(), {eq_rec_lvl1, eq_rec_lvl2});
eq_rec = mk_app(eq_rec, A, lhs, tformer);
buffer<expr> new_hyps;
new_hyps.append(non_deps);
expr new_type = instantiate(abstract_local(deps_g_type, rhs), lhs);
store_rename(rhs, lhs);
replace(m_imps, rhs, lhs);
if (!m_proof_irrel) {
new_type = abstract_local(new_type, Heq);
new_type = instantiate(new_type, mk_refl(m_tc, lhs));
}
buffer<expr> new_deps;
for (unsigned i = 0; i < deps.size(); i++) {
expr new_hyp = mk_local(m_ngen.next(), binding_name(new_type), binding_domain(new_type),
binding_info(new_type));
new_hyps.push_back(new_hyp);
new_deps.push_back(new_hyp);
new_type = instantiate(binding_body(new_type), new_hyp);
}
replace(m_imps, deps, new_deps);
lean_assert(deps.size() == new_deps.size());
store_renames(deps, new_deps);
expr new_mvar = mk_metavar(m_ngen.next(), Pi(new_hyps, new_type));
expr new_meta = mk_app(new_mvar, new_hyps);
goal new_g(new_meta, new_type);
expr eq_rec_minor = mk_app(new_mvar, non_deps);
eq_rec = mk_app(eq_rec, eq_rec_minor, rhs, Heq);
expr val = mk_app(eq_rec, deps);
assign(g, val);
return unify_eqs(new_g, neqs-1);
}
} else if (is_local(lhs)) {
// flip equation and reduce to previous case
expr symm_eq = mk_eq(rhs, lhs).first;
hyps.pop_back(); // remove processed equality
if (!depends_on(g_type, Heq)) {
expr new_type = mk_arrow(symm_eq, g_type);
expr new_mvar = mk_metavar(m_ngen.next(), Pi(hyps, new_type));
expr new_meta = mk_app(new_mvar, hyps);
goal new_g(new_meta, new_type);
expr Heq_inv = mk_symm(m_tc, Heq);
expr val = mk_app(new_meta, Heq_inv);
assign(g, val);
return unify_eqs(new_g, neqs);
} else {
// Let C[Heq] be the original conclusion which depends on the equality eq being processed.
expr new_Heq = update_mlocal(Heq, symm_eq);
expr new_Heq_inv = mk_symm(m_tc, new_Heq);
expr new_type = Pi(new_Heq, instantiate(abstract_local(g_type, Heq), new_Heq_inv));
expr new_mvar = mk_metavar(m_ngen.next(), Pi(hyps, new_type));
expr new_meta = mk_app(new_mvar, hyps);
goal new_g(new_meta, new_type);
// Then, we have
// new_meta : Pi (new_Heq : rhs = lhs), C[symm new_Heq]
expr Heq_inv = mk_symm(m_tc, Heq);
expr val = mk_app(new_meta, Heq_inv);
// val : C[symm (symm Heq)]
// Remark: in proof irrelevant mode (symm (symm Heq)) is definitionally equal to Heq
if (!m_proof_irrel) {
expr C = Fun(Heq, g_type);
level A_lvl = sort_level(m_tc.ensure_type(A).first);
level g_lvl = sort_level(m_tc.ensure_type(g_type).first);
expr elim_inv_inv = mk_constant(get_eq_elim_inv_inv_name(), {A_lvl, g_lvl});
val = mk_app({elim_inv_inv, A, lhs, rhs, C, Heq, val});
// val : C[Heq] as we wanted
}
assign(g, val);
return unify_eqs(new_g, neqs);
}
}
throw_unify_eqs_failure(g);
}
auto unify_eqs(list<goal> const & gs, list<list<expr>> args, list<implementation_list> imps) ->
std::tuple<list<goal>, list<list<expr>>, list<implementation_list>, list<rename_map>> {
lean_assert(length(gs) == length(imps));
unsigned neqs = m_nindices + (m_dep_elim ? 1 : 0);
buffer<goal> new_goals;
buffer<list<expr>> new_args;
buffer<implementation_list> new_imps;
buffer<rename_map> new_renames;
for (goal const & g : gs) {
flet<rename_map> set1(m_renames, rename_map());
flet<implementation_list> set2(m_imps, head(imps));
m_c_args.clear();
to_buffer(head(args), m_c_args);
if (optional<goal> new_g = unify_eqs(g, neqs)) {
new_goals.push_back(*new_g);
list<expr> new_as = to_list(m_c_args);
new_args.push_back(new_as);
new_imps.push_back(m_imps);
new_renames.push_back(m_renames);
}
imps = tail(imps);
args = tail(args);
}
return std::make_tuple(to_list(new_goals), to_list(new_args), to_list(new_imps), to_list(new_renames));
}
std::pair<goal, rename_map> intro_deps(goal const & g, buffer<expr> const & deps) {
buffer<expr> hyps;
g.get_hyps(hyps);
buffer<expr> new_hyps;
new_hyps.append(hyps);
rename_map rs;
expr g_type = g.get_type();
for (expr const & d : deps) {
expr type = binding_domain(g_type);
expr new_d = mk_local(m_ngen.next(), get_unused_name(local_pp_name(d), new_hyps), type, local_info(d));
rs.insert(mlocal_name(d), mlocal_name(new_d));
new_hyps.push_back(new_d);
g_type = instantiate(binding_body(g_type), new_d);
}
expr new_meta = mk_app(mk_metavar(m_ngen.next(), Pi(new_hyps, g_type)), new_hyps);
goal new_g(new_meta, g_type);
unsigned ndeps = deps.size();
expr val = Fun(ndeps, new_hyps.end() - ndeps, new_meta);
assign(g, val);
return mk_pair(new_g, rs);
}
std::pair<list<goal>, list<rename_map>> intro_deps(list<goal> const & gs, buffer<expr> const & deps) {
buffer<goal> new_goals;
buffer<rename_map> new_rs;
for (goal const & g : gs) {
auto p = intro_deps(g, deps);
new_goals.push_back(p.first);
new_rs.push_back(p.second);
}
return mk_pair(to_list(new_goals), to_list(new_rs));
}
goal clear_hypothesis(goal const & g, name const & h) {
if (auto p = g.find_hyp_from_internal_name(h)) {
expr const & h = p->first;
unsigned i = p->second;
buffer<expr> hyps;
g.get_hyps(hyps);
hyps.erase(hyps.size() - i - 1);
if (depends_on(g.get_type(), h) || depends_on(i, hyps.end() - i, h)) {
return g; // other hypotheses or result type depend on h
}
expr new_type = g.get_type();
expr new_meta = mk_app(mk_metavar(m_ngen.next(), Pi(hyps, new_type)), hyps);
goal new_g(new_meta, new_type);
assign(g, new_meta);
return new_g;
} else {
return g;
}
}
// Remove hypothesis of the form (H : a = a)
goal remove_eq_refl_hypotheses(goal g) {
buffer<name> to_remove;
buffer<expr> hyps;
g.get_hyps(hyps);
for (expr const & h : hyps) {
expr const & h_type = mlocal_type(h);
if (!is_eq(h_type))
continue;
expr const & lhs = app_arg(app_fn(h_type));
expr const & rhs = app_arg(h_type);
if (lhs == rhs)
to_remove.push_back(mlocal_name(h));
}
for (name const & h : to_remove) {
g = clear_hypothesis(g, h);
}
return g;
}
list<goal> clear_hypothesis(list<goal> const & gs, list<rename_map> rs, name const & h_name, expr const & h_type) {
buffer<goal> new_gs;
optional<name> lhs_name; // If h_type is of the form lhs = rhs, and lhs is also a hypothesis, then we also remove it.
if (is_eq(h_type) && is_local(app_arg(app_fn(h_type)))) {
lhs_name = mlocal_name(app_arg(app_fn(h_type)));
}
for (goal const & g : gs) {
rename_map const & m = head(rs);
goal new_g = clear_hypothesis(g, m.find(h_name));
if (lhs_name)
new_g = clear_hypothesis(new_g, *lhs_name);
if (!m_proof_irrel)
new_g = remove_eq_refl_hypotheses(new_g);
new_gs.push_back(new_g);
rs = tail(rs);
}
return to_list(new_gs);
}
public:
inversion_tac(environment const & env, io_state const & ios, name_generator const & ngen,
type_checker & tc, substitution const & subst, list<name> const & ids,
bool throw_tactic_ex, bool clear_elim):
m_env(env), m_ios(ios), m_tc(tc), m_ids(ids),
m_ngen(ngen), m_subst(subst), m_throw_tactic_exception(throw_tactic_ex),
m_clear_elim(clear_elim) {
m_proof_irrel = m_env.prop_proof_irrel();
}
inversion_tac(environment const & env, io_state const & ios, type_checker & tc, bool clear_elim):
inversion_tac(env, ios, tc.mk_ngen(), tc, substitution(), list<name>(), false, clear_elim) {}
typedef inversion::result result;
optional<result> execute(goal const & g, expr const & h, implementation_list const & imps) {
try {
expr h_type = whnf(mlocal_type(h));
if (!is_inversion_applicable(h_type))
return optional<result>();
if (has_indep_indices(g, h, h_type)) {
buffer<expr> deps;
goal g1 = generalize_dependecies(g, h, deps);
auto gs_imps_pair = apply_cases_on(g1, imps, true);
list<goal> gs2 = gs_imps_pair.first;
list<implementation_list> new_imps = gs_imps_pair.second;
auto gs_args_pair = intros_minors_args(gs2);
list<goal> gs3 = gs_args_pair.first;
list<list<expr>> args = gs_args_pair.second;
auto gs_rs_pair = intro_deps(gs3, deps);
list<goal> gs4 = gs_rs_pair.first;
list<rename_map> rs = gs_rs_pair.second;
return optional<result>(result(gs4, args, new_imps, rs, m_ngen, m_subst));
} else {
goal g1 = generalize_indices(g, h, h_type);
auto gs_imps_pair = apply_cases_on(g1, imps, false);
list<goal> gs2 = gs_imps_pair.first;
list<implementation_list> new_imps = gs_imps_pair.second;
auto gs_args_pair = intros_minors_args(gs2);
list<goal> gs3 = gs_args_pair.first;
list<list<expr>> args = gs_args_pair.second;
list<goal> gs4;
list<rename_map> rs;
std::tie(gs4, args, new_imps, rs) = unify_eqs(gs3, args, new_imps);
gs4 = clear_hypothesis(gs4, rs, mlocal_name(h), h_type);
return optional<result>(result(gs4, args, new_imps, rs, m_ngen, m_subst));
}
} catch (inversion_exception & ex) {
return optional<result>();
}
}
optional<result> execute(goal const & g, name const & n, implementation_list const & imps) {
auto p = g.find_hyp(n);
if (!p) {
if (m_throw_tactic_exception)
throw tactic_exception(sstream() << "invalid 'cases' tactic, unknown hypothesis '" << n << "'");
return optional<result>();
}
expr const & h = p->first;
return execute(g, h, imps);
}
};
namespace inversion {
optional<result> apply(environment const & env, io_state const & ios, type_checker & tc,
goal const & g, expr const & h, implementation_list const & imps, bool clear_elim) {
return inversion_tac(env, ios, tc, clear_elim).execute(g, h, imps);
}
}
tactic inversion_tactic(name const & n, list<name> const & ids) {
auto fn = [=](environment const & env, io_state const & ios, proof_state const & ps) -> optional<proof_state> {
goals const & gs = ps.get_goals();
if (empty(gs))
return none_proof_state();
goal g = head(gs);
goals tail_gs = tail(gs);
name_generator ngen = ps.get_ngen();
std::unique_ptr<type_checker> tc = mk_type_checker(env, ngen.mk_child());
bool clear_elim = true;
inversion_tac tac(env, ios, ngen, *tc, ps.get_subst(), ids, ps.report_failure(), clear_elim);
if (auto res = tac.execute(g, n, implementation_list())) {
proof_state new_s(ps, append(res->m_goals, tail_gs), res->m_subst, res->m_ngen);
return some_proof_state(new_s);
} else {
return none_proof_state();
}
};
return tactic01(fn);
}
void initialize_inversion_tactic() {
register_tac(get_tactic_cases_name(),
[](type_checker &, elaborate_fn const &, expr const & e, pos_info_provider const *) {
name n = tactic_expr_to_id(app_arg(app_fn(e)), "invalid 'cases' tactic, argument must be an identifier");
buffer<name> ids;
get_tactic_id_list_elements(app_arg(e), ids, "invalid 'cases' tactic, list of identifiers expected");
return inversion_tactic(n, to_list(ids.begin(), ids.end()));
});
}
void finalize_inversion_tactic() {}
}