refactor(library/tactic/simp_lemmas): new caching mechanism

This commit is contained in:
Leonardo de Moura 2016-10-06 20:20:01 -07:00
parent 1e93c2c235
commit d747fcb17c
14 changed files with 193 additions and 154 deletions

View file

@ -14,11 +14,7 @@ meta constant simp_lemmas.erase : simp_lemmas → list name → simp_lemmas
namespace tactic
open list nat
/- Create a data-structure containing a simp lemma for every constant in the first list of
attributes, and a congr lemma for every constant in the second list of attributes.
Lemmas with type `<lhs> <eqv_rel> <rhs>` are indexed using the head-symbol of `<lhs>`,
computed with respect to the given transparency setting. -/
meta constant mk_simp_lemmas_core : transparency → list name → list name → tactic simp_lemmas
meta constant mk_default_simp_lemmas_core : transparency → tactic simp_lemmas
/- (simp_lemmas_insert_core m lemmas lemma priority) adds the given lemma to the set simp_lemmas. -/
meta constant simp_lemmas_insert_core : transparency → simp_lemmas → expr → tactic simp_lemmas
/- (simp_lemmas_insert_constant_core m lemmas cname) -/
@ -28,7 +24,7 @@ meta def simp_lemmas_insert_constant : simp_lemmas → name → tactic simp_lemm
simp_lemmas_insert_constant_core reducible
meta def mk_simp_lemmas : tactic simp_lemmas :=
mk_simp_lemmas_core reducible [`simp] [`congr]
mk_default_simp_lemmas_core reducible
meta def simp_lemmas_add_extra : transparency → simp_lemmas → list expr → tactic simp_lemmas
| m sls [] := return sls

View file

@ -471,44 +471,21 @@ static void print_rfl_lemmas(parser & p) {
}
static void print_simp_rules(parser & p) {
type_context aux_tctx(p.env(), p.get_options());
name attr = p.check_id_next("invalid 'print [simp]' command, identifier expected");
buffer<name> attrs;
attrs.push_back(attr);
buffer<name> no_congr;
simp_lemmas slss = get_simp_lemmas(aux_tctx, attrs, no_congr);
simp_lemmas slss = get_simp_lemmas(p.env(), transparency_mode::Reducible, attr);
type_checker tc(p.env());
auto out = regular(p.env(), p.ios(), tc);
out << slss.pp_simp(out.get_formatter());
}
static void print_congr_rules(parser & p) {
type_context aux_tctx(p.env(), p.get_options());
name attr = p.check_id_next("invalid 'print [congr]' command, identifier expected");
buffer<name> no_simp;
buffer<name> attrs;
attrs.push_back(attr);
simp_lemmas slss = get_simp_lemmas(aux_tctx, no_simp, attrs);
simp_lemmas slss = get_simp_lemmas(p.env(), transparency_mode::Reducible, attr);
type_checker tc(p.env());
auto out = regular(p.env(), p.ios(), tc);
out << slss.pp_congr(out.get_formatter());
}
static void print_simp_extensions(parser & p) {
type_checker tc(p.env());
auto out = regular(p.env(), p.ios(), tc);
out << pp_simp_extensions(p.env());
}
#if 0
static void print_light_rules(parser & p) {
type_checker tc(p.env());
auto out = regular(p.env(), p.ios(), tc);
light_rule_set lrs = get_light_rule_set(p.env());
out << lrs;
}
#endif
static void print_aliases(parser const & p) {
std::ostream & out = p.ios().get_regular_stream();
for_each_expr_alias(p.env(), [&](name const & n, list<name> const & as) {
@ -647,20 +624,16 @@ environment print_cmd(parser & p) {
auto name = p.check_id_next("invalid attribute declaration, identifier expected");
p.check_token_next(get_rbracket_tk(), "invalid 'print [<attr>]', ']' expected");
if (name == "recursor")
if (name == "recursor") {
print_recursor_info(p);
else if (name == "unify")
} else if (name == "unify") {
print_unification_hints(p);
else if (name == "defeq")
} else if (name == "defeq") {
print_rfl_lemmas(p);
else if (name == "simp")
} else if (name == "simp") {
print_simp_rules(p);
else if (name == "simp_ext")
print_simp_extensions(p);
else if (name == "congr")
} else if (name == "congr") {
print_congr_rules(p);
else if (name == "light") {
// print_light_rules(p);
} else {
if (!is_attribute(p.env(), name))
throw parser_error(sstream() << "unknown attribute [" << name << "]", pos);

View file

@ -34,14 +34,13 @@ static std::vector<pair<name, name>> * g_incomp = nullptr;
static std::string * g_key = nullptr;
static bool is_system_attribute(name const & attr) {
return g_system_attributes->find(attr) != nullptr;
bool is_system_attribute(name const & attr) {
return g_system_attributes->contains(attr);
}
void register_system_attribute(attribute_ptr attr) {
lean_assert(!is_system_attribute(attr->get_name()));
(*g_system_attributes)[attr->get_name()] = attr;
}
bool is_attribute(environment const & env, name const & attr) {
return is_system_attribute(attr) || g_user_attribute_ext->get_attributes(env).find(attr) != nullptr;
}

View file

@ -233,6 +233,7 @@ void register_system_attribute(Attribute attr) {
}
void register_incompatible(char const * attr1, char const * attr2);
bool is_system_attribute(name const & attr_name);
bool is_attribute(environment const & env, name const & attr);
attribute const & get_attribute(environment const & env, name const & attr);
attribute const & get_system_attribute(name const & attr);

View file

@ -27,8 +27,8 @@ Author: Daniel Selsam
#include "library/constructions/has_sizeof.h"
namespace lean {
static name * g_simp_sizeof = nullptr;
static simp_lemmas_token g_simp_sizeof_tk;
static basic_attribute const & get_simp_sizeof_attribute() {
return static_cast<basic_attribute const &>(get_system_attribute(*g_simp_sizeof));
@ -293,16 +293,13 @@ name simp_sizeof_attribute_name() {
return *g_simp_sizeof;
}
simp_lemmas get_sizeof_simp_lemmas(type_context & tctx) {
buffer<name> simp_attrs, congr_attrs;
simp_attrs.push_back(simp_sizeof_attribute_name());
return get_simp_lemmas(tctx, simp_attrs, congr_attrs);
simp_lemmas get_sizeof_simp_lemmas(environment const & env, transparency_mode m) {
return get_simp_lemmas(env, m, g_simp_sizeof_tk);
}
void initialize_has_sizeof() {
g_simp_sizeof = new name{"simp", "sizeof"};
register_system_attribute(basic_attribute::with_check(*g_simp_sizeof, "simplification lemma", on_add_simp_lemma));
g_simp_sizeof = new name{"simp", "sizeof"};
g_simp_sizeof_tk = register_simp_attribute("ssizeof", {*g_simp_sizeof}, {});
register_trace_class(name({"constructions", "has_sizeof"}));
}

View file

@ -16,7 +16,7 @@ environment mk_has_sizeof(environment const & env, name const & ind_name);
name mk_has_sizeof_name(name const & ind_name);
name mk_sizeof_spec_name(name const & ir_name);
name simp_sizeof_attribute_name();
simp_lemmas get_sizeof_simp_lemmas(type_context & tctx);
simp_lemmas get_sizeof_simp_lemmas(environment const & env, transparency_mode m);
environment set_simp_sizeof(environment const & env, name const & n);
void initialize_has_sizeof();

View file

@ -1689,7 +1689,7 @@ public:
construct_inner_decl();
add_inner_decl();
check_elim_to_type();
m_sizeof_lemmas = get_sizeof_simp_lemmas(m_tctx);
m_sizeof_lemmas = get_sizeof_simp_lemmas(m_tctx.env(), m_tctx.mode());
define_nested_inds();

View file

@ -16,6 +16,7 @@ Author: Leonardo de Moura
#include "kernel/find_fn.h"
#include "kernel/instantiate.h"
#include "library/trace.h"
#include "library/reducible.h"
#include "library/num.h"
#include "library/cache_helper.h"
#include "library/scoped_ext.h"
@ -31,32 +32,7 @@ Author: Leonardo de Moura
#include "library/tactic/simplifier/simp_lemmas.h"
namespace lean {
/* Caching */
struct simp_lemma_cache {
typedef name_hash_map<simp_lemmas> cache;
environment m_env;
cache m_simp_lemma_cache;
cache m_congr_lemma_cache;
simp_lemma_cache(environment const & env): m_env(env) {}
environment const & env() const { return m_env; }
cache & simp_cache() { return m_simp_lemma_cache; }
cache & congr_cache() { return m_congr_lemma_cache; }
};
typedef cache_compatibility_helper<simp_lemma_cache> simp_lemma_cache_helper;
MK_THREAD_LOCAL_GET_DEF(simp_lemma_cache_helper, get_slch);
simp_lemma_cache & get_simp_lemma_cache_for(type_context const & ctx) {
return get_slch().get_cache_for(ctx);
}
/* Validation */
LEAN_THREAD_VALUE(bool, g_throw_ex, false);
void validate_simp(type_context & tctx, name const & n);
void validate_congr(type_context & tctx, name const & n);
@ -72,7 +48,6 @@ void on_add_congr_lemma(environment const & env, name const & c, bool) {
}
/* Getters/checkers */
static void report_failure(sstream const & strm) {
if (g_throw_ex){
throw exception(strm);
@ -288,7 +263,8 @@ simp_lemmas add_poly(type_context & tctx, simp_lemmas const & s, name const & id
return add_core(tmp_tctx, s, id, priority);
}
simp_lemmas add(type_context & tctx, simp_lemmas const & s, name const & id, expr const & e, expr const & h, unsigned priority) {
simp_lemmas add(type_context & tctx, simp_lemmas const & s, name const & id, expr const & e, expr const & h,
unsigned priority) {
tmp_type_context tmp_tctx(tctx);
return add_core(tmp_tctx, s, id, list<level>(), e, h, priority);
}
@ -574,62 +550,170 @@ format simp_lemmas::pp(formatter const & fmt) const {
return pp(fmt, format(), true, true);
}
simp_lemmas get_simp_lemmas_for_attr(simp_lemma_cache & sl_cache, type_context & tctx, name const & simp_attr) {
auto it = sl_cache.simp_cache().find(simp_attr);
if (it != sl_cache.simp_cache().end())
return it->second;
auto const & attr = get_attribute(tctx.env(), simp_attr);
simp_lemmas r;
simp_lemmas get_simp_lemmas_from_attribute(type_context & ctx, name const & attr_name, simp_lemmas result) {
auto const & attr = get_attribute(ctx.env(), attr_name);
buffer<name> simp_lemmas;
attr.get_instances(tctx.env(), simp_lemmas);
attr.get_instances(ctx.env(), simp_lemmas);
unsigned i = simp_lemmas.size();
while (i > 0) {
i--;
name const & id = simp_lemmas[i];
tmp_type_context tmp_tctx(tctx);
r = add_core(tmp_tctx, r, id, attr.get_prio(tctx.env(), id));
// TODO(Leo): fix the following hack
tmp_type_context tmp_tctx(ctx);
result = add_core(tmp_tctx, result, id, attr.get_prio(ctx.env(), id));
}
sl_cache.simp_cache().insert({simp_attr, r});
return r;
return result;
}
simp_lemmas get_congr_lemmas_for_attr(simp_lemma_cache & sl_cache, type_context & tctx, name const & congr_attr) {
auto it = sl_cache.congr_cache().find(congr_attr);
if (it != sl_cache.congr_cache().end())
return it->second;
auto const & attr = get_attribute(tctx.env(), congr_attr);
simp_lemmas r;
simp_lemmas get_congr_lemmas_from_attribute(type_context & ctx, name const & attr_name, simp_lemmas result) {
auto const & attr = get_attribute(ctx.env(), attr_name);
buffer<name> congr_lemmas;
attr.get_instances(tctx.env(), congr_lemmas);
attr.get_instances(ctx.env(), congr_lemmas);
unsigned i = congr_lemmas.size();
while (i > 0) {
i--;
name const & id = congr_lemmas[i];
tmp_type_context tmp_tctx(tctx);
r = add_congr_core(tmp_tctx, r, id, attr.get_prio(tctx.env(), id));
tmp_type_context tmp_tctx(ctx);
result = add_congr_core(tmp_tctx, result, id, attr.get_prio(ctx.env(), id));
}
sl_cache.congr_cache().insert({congr_attr, r});
return r;
return result;
}
simp_lemmas get_simp_lemmas(type_context & tctx, buffer<name> const & simp_attrs, buffer<name> const & congr_attrs) {
simp_lemma_cache & sl_cache = get_simp_lemma_cache_for(tctx);
simp_lemmas r;
struct simp_lemmas_config {
std::vector<name> m_simp_attrs;
std::vector<name> m_congr_attrs;
};
// Optimization for the case when there is only one simp attr
// (we avoid the redundant head_map_prio inserts)
if (simp_attrs.size() > 0)
r = get_simp_lemmas_for_attr(sl_cache, tctx, simp_attrs[0]);
static std::vector<simp_lemmas_config> * g_simp_lemmas_configs = nullptr;
static name_map<unsigned> * g_name2simp_token = nullptr;
static simp_lemmas_token g_default_token;
for (unsigned i = 1; i < simp_attrs.size(); ++i)
r = join(r, get_simp_lemmas_for_attr(sl_cache, tctx, simp_attrs[i]));
simp_lemmas_token register_simp_attribute(name const & user_name, std::initializer_list<name> const & simp_attrs, std::initializer_list<name> const & congr_attrs) {
simp_lemmas_config cfg;
for (name const & attr_name : simp_attrs) {
cfg.m_simp_attrs.push_back(attr_name);
if (!is_system_attribute(attr_name)) {
register_system_attribute(basic_attribute::with_check(attr_name, "simplification lemma", on_add_simp_lemma));
}
}
for (name const & attr_name : congr_attrs) {
cfg.m_congr_attrs.push_back(attr_name);
if (!is_system_attribute(attr_name)) {
register_system_attribute(basic_attribute::with_check(attr_name, "congruence lemma", on_add_congr_lemma));
}
}
simp_lemmas_token tk = g_simp_lemmas_configs->size();
g_simp_lemmas_configs->push_back(cfg);
g_name2simp_token->insert(user_name, tk);
return tk;
}
for (unsigned i = 0; i < congr_attrs.size(); ++i)
r = join(r, get_congr_lemmas_for_attr(sl_cache, tctx, congr_attrs[i]));
simp_lemmas_config const & get_simp_lemmas_config(simp_lemmas_token tk) {
lean_assert(tk < g_simp_lemmas_configs->size());
return (*g_simp_lemmas_configs)[tk];
}
return r;
/* This is the cache for internally used simp_lemma collections */
class simp_lemmas_cache {
struct entry {
environment m_env;
std::vector<unsigned> m_fingerprints;
unsigned m_reducibility_fingerprint;
optional<simp_lemmas> m_lemmas;
entry(environment const & env):
m_env(env), m_reducibility_fingerprint(0) {}
};
std::vector<entry> m_entries[4];
public:
void expand(environment const & env, transparency_mode m, unsigned new_sz) {
unsigned midx = static_cast<unsigned>(m);
for (unsigned tk = m_entries[midx].size(); tk < new_sz; tk++) {
auto & cfg = get_simp_lemmas_config(tk);
m_entries[midx].emplace_back(env);
auto & entry = m_entries[midx].back();
entry.m_fingerprints.resize(cfg.m_simp_attrs.size() + cfg.m_congr_attrs.size());
}
}
simp_lemmas mk_lemmas(environment const & env, transparency_mode m, entry & C, simp_lemmas_token tk) {
lean_trace("simp_lemmas_cache", tout() << "make simp lemmas [" << tk << "]\n";);
type_context ctx(env, m);
C.m_env = env;
auto & cfg = get_simp_lemmas_config(tk);
simp_lemmas lemmas;
unsigned i = 0;
for (name const & attr_name : cfg.m_simp_attrs) {
lemmas = get_simp_lemmas_from_attribute(ctx, attr_name, lemmas);
C.m_fingerprints[i] = get_attribute_fingerprint(env, attr_name);
i++;
}
for (name const & attr_name : cfg.m_congr_attrs) {
lemmas = get_congr_lemmas_from_attribute(ctx, attr_name, lemmas);
C.m_fingerprints[i] = get_attribute_fingerprint(env, attr_name);
i++;
}
C.m_lemmas = lemmas;
C.m_reducibility_fingerprint = get_reducibility_fingerprint(env);
return lemmas;
}
simp_lemmas lemmas_of(entry const & C, simp_lemmas_token tk) {
lean_trace("simp_lemmas_cache", tout() << "reusing cached simp lemmas [" << tk << "]\n";);
return *C.m_lemmas;
}
bool is_compatible(entry const & C, environment const & env, simp_lemmas_token tk) {
if (!env.is_descendant(C.m_env))
return false;
if (get_reducibility_fingerprint(env) != C.m_reducibility_fingerprint)
return false;
auto & cfg = get_simp_lemmas_config(tk);
unsigned i = 0;
for (name const & attr_name : cfg.m_simp_attrs) {
if (get_attribute_fingerprint(env, attr_name) != C.m_fingerprints[i])
return false;
i++;
}
for (name const & attr_name : cfg.m_congr_attrs) {
if (get_attribute_fingerprint(env, attr_name) != C.m_fingerprints[i])
return false;
i++;
}
return true;
}
simp_lemmas get(environment const & env, transparency_mode m, simp_lemmas_token tk) {
lean_assert(tk < g_simp_lemmas_configs->size());
unsigned midx = static_cast<unsigned>(m);
if (tk >= m_entries[midx].size()) expand(env, m, tk+1);
lean_assert(tk < m_entries[midx].size());
entry & C = m_entries[midx][tk];
if (!C.m_lemmas) return mk_lemmas(env, m, C, tk);
if (is_eqp(env, C.m_env)) return lemmas_of(C, tk);
if (!is_compatible(C, env, tk)) {
lean_trace("simp_lemmas_cache", tout() << "creating new cache\n";);
return mk_lemmas(env, m, C, tk);
}
return lemmas_of(C, tk);
}
};
MK_THREAD_LOCAL_GET_DEF(simp_lemmas_cache, get_cache);
simp_lemmas get_simp_lemmas(environment const & env, transparency_mode m, simp_lemmas_token tk) {
return get_cache().get(env, m, tk);
}
simp_lemmas get_default_simp_lemmas(environment const & env, transparency_mode m) {
return get_simp_lemmas(env, m, g_default_token);
}
simp_lemmas get_simp_lemmas(environment const & env, transparency_mode m, name const & tk_name) {
if (simp_lemmas_token const * tk = g_name2simp_token->find(tk_name))
return get_simp_lemmas(env, m, *tk);
else
throw exception(sstream() << "unknown simp_lemmas collection '" << tk_name << "'");
}
struct vm_simp_lemmas : public vm_external {
@ -649,13 +733,11 @@ vm_obj to_obj(simp_lemmas const & idx) {
return mk_vm_external(new (get_vm_allocator().allocate(sizeof(vm_simp_lemmas))) vm_simp_lemmas(idx));
}
vm_obj tactic_mk_simp_lemmas(vm_obj const & m, vm_obj const & sattrs, vm_obj const & cattrs, vm_obj const & s) {
vm_obj tactic_mk_default_simp_lemmas(vm_obj const & m, vm_obj const & s) {
LEAN_TACTIC_TRY;
type_context ctx = mk_type_context_for(s, m);
buffer<name> simp_attrs, congr_attrs;
to_buffer(to_list_name(sattrs), simp_attrs);
to_buffer(to_list_name(cattrs), congr_attrs);
return mk_tactic_success(to_obj(get_simp_lemmas(ctx, simp_attrs, congr_attrs)), to_tactic_state(s));
environment const & env = to_tactic_state(s).env();
simp_lemmas r = get_default_simp_lemmas(env, to_transparency_mode(m));
return mk_tactic_success(to_obj(r), to_tactic_state(s));
LEAN_TACTIC_CATCH(to_tactic_state(s));
}
@ -827,17 +909,21 @@ static vm_obj tactic_simp_lemmas_apply(vm_obj const & m, vm_obj const & sls, vm_
}
void initialize_simp_lemmas() {
g_simp_lemmas_configs = new std::vector<simp_lemmas_config>();
g_name2simp_token = new name_map<unsigned>();
g_default_token = register_simp_attribute("default", {"simp"}, {"congr"});
register_trace_class("simp_lemmas");
DECLARE_VM_BUILTIN(name({"simp_lemmas", "mk"}), simp_lemmas_mk);
DECLARE_VM_BUILTIN(name({"simp_lemmas", "join"}), simp_lemmas_join);
DECLARE_VM_BUILTIN(name({"simp_lemmas", "erase"}), simp_lemmas_erase);
DECLARE_VM_BUILTIN(name({"tactic", "mk_simp_lemmas_core"}), tactic_mk_simp_lemmas);
DECLARE_VM_BUILTIN(name({"tactic", "mk_default_simp_lemmas_core"}), tactic_mk_default_simp_lemmas);
DECLARE_VM_BUILTIN(name({"tactic", "simp_lemmas_insert_core"}), tactic_simp_lemmas_insert);
DECLARE_VM_BUILTIN(name({"tactic", "simp_lemmas_insert_constant_core"}), tactic_simp_lemmas_insert_constant);
DECLARE_VM_BUILTIN(name({"tactic", "simp_lemmas_apply_core"}), tactic_simp_lemmas_apply);
register_system_attribute(basic_attribute::with_check("simp", "simplification lemma", on_add_simp_lemma));
register_system_attribute(basic_attribute::with_check("congr", "congruence lemma", on_add_congr_lemma));
}
void finalize_simp_lemmas() {
delete g_simp_lemmas_configs;
delete g_name2simp_token;
}
}

View file

@ -141,7 +141,17 @@ public:
format pp(formatter const & fmt) const;
};
simp_lemmas get_simp_lemmas(type_context & tctx, buffer<name> const & simp_attrs, buffer<name> const & congr_attrs);
typedef unsigned simp_lemmas_token;
/* Create a token for accessing a simp_lemmas that contains the given simp and attributes.
The attributes are automatically registered in the system IF they have not already been registered. */
simp_lemmas_token register_simp_attribute(name const & user_name,
std::initializer_list<name> const & simp_attrs,
std::initializer_list<name> const & congr_attrs);
simp_lemmas get_simp_lemmas(environment const & env, transparency_mode m, simp_lemmas_token tk);
simp_lemmas get_default_simp_lemmas(environment const & env, transparency_mode m);
simp_lemmas get_simp_lemmas(environment const & env, transparency_mode m, name const & tk_name);
simp_lemmas add_poly(type_context & tctx, simp_lemmas const & s, name const & id, unsigned priority);
simp_lemmas add(type_context & tctx, simp_lemmas const & s, name const & id, expr const & e, expr const & h, unsigned priority);

View file

@ -13,5 +13,5 @@ namespace foo
attribute [simp] nat.add_comm
open nat
print "---------"
print [simp] simp
print [simp] default
end foo

View file

@ -9,6 +9,6 @@ axiom bla : ∀ x, f x = 2
attribute [simp] foo
attribute [simp] bla
print [simp] simp
print [simp] default
example : f 5 = 2 := by simp

View file

@ -5,7 +5,7 @@ theorem forall_congr_prop_eq {P₁ P₂ Q₁ Q₂ : Prop} :
P₁ = P₂ → (P₂ → Q₁ = Q₂) → (P₁ → Q₁) = (P₂ → Q₂) :=
sorry
print [congr] congr
print [congr] default
example (A : Type) (a b c : A) : (a = b) → (a = c) → a = b := by (simp >> intros >> reflexivity)
example (A : Type) (a b c : A) : (a = c) → (a = b) → a = b := by (simp >> intros >> reflexivity)

View file

@ -1,23 +0,0 @@
open list tactic option
constants (A : Type.{1}) (x y z : A) (Hy : x = y) (Hz : y = z)
constants (f₁ : A → A) (f₂ : A → A → A)
meta definition simp_x_to_y : tactic unit := mk_eq_simp_ext $
λ e, do res ← mk_const `y,
pf ← mk_const `Hy,
return (res, pf)
meta definition simp_y_to_z : tactic unit := mk_eq_simp_ext $
λ e, do res ← mk_const `z,
pf ← mk_const `Hz,
return (res, pf)
register_simp_ext x simp_x_to_y
register_simp_ext y simp_y_to_z
print [simp_ext]
example : x = z := by simp
example : f₁ x = f₁ y := by simp
example : f₁ (f₂ x y) = f₁ (f₂ z z) := by simp
example : f₁ (f₂ x y) = f₁ (f₂ y x) := by simp

View file

@ -20,8 +20,8 @@ constants (x y z : A) (f g h : A → A)
attribute [simp] H₁ H₂
attribute [congr] Hf Hg Hh
print [simp] simp
print [congr] congr
print [simp] default
print [congr] default
meta definition relsimp_core (e : expr) : tactic (expr × expr) :=
do simp_lemmas ← mk_simp_lemmas,