Skip to content
This repository was archived by the owner on Jul 24, 2024. It is now read-only.

Commit ce7c94f

Browse files
fpvandoornmergify[bot]
authored andcommitted
feat(reduce_projections): add reduce_projections attribute (#1402)
* feat(reduce_projections): add attribute This attribute automatically generates simp-lemmas for an instance of a structure stating what the projections of this instance are * add tactic doc * use lean code blocks * missing `lemma` * checkpoint * recursively apply reduce_projections and eta-reduce structures * reviewer comments, shorten name new name is simps * avoid name clashes * remove recursive import * fix eta-expansion bug * typo * apply whnf to type * add test * replace nm by decl_name
1 parent efd5ab2 commit ce7c94f

File tree

8 files changed

+449
-1
lines changed

8 files changed

+449
-1
lines changed

docs/tactics.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,3 +1208,27 @@ When possible, make `foo` non-private rather than using this feature.
12081208
bug is often indicated by a message `nested exception message: tactic failed, there are no goals to be solved`,and solved by appending `using_well_founded wf_tacs` to the recursive definition.
12091209
See also additional documentation of `using_well_founded` in
12101210
[docs/extras/well_founded_recursion.md](extras/well_founded_recursion.md).
1211+
1212+
### simps
1213+
1214+
* The `@[simps]` attribute automatically derives lemmas specifying the projections of the declaration.
1215+
* Example:
1216+
```lean
1217+
@[simps] def refl (α) : α ≃ α := ⟨id, id, λ x, rfl, λ x, rfl⟩
1218+
```
1219+
derives two simp-lemmas:
1220+
```lean
1221+
@[simp] lemma refl_to_fun (α) : (refl α).to_fun = id
1222+
@[simp] lemma refl_inv_fun (α) : (refl α).inv_fun = id
1223+
```
1224+
* It does not derive simp-lemmas for the prop-valued projections.
1225+
* It will automatically reduce newly created beta-redexes, but not unfold any definitions.
1226+
* If one of the fields itself is a structure, this command will recursively create
1227+
simp-lemmas for all fields in that structure.
1228+
* If one of the values is an eta-expanded structure, we will eta-reduce this structure.
1229+
* You can use `@[simps lemmas_only]` to derive the lemmas, but not mark them
1230+
as simp-lemmas.
1231+
* If one of the projections is marked as a coercion, the generated lemmas do *not* use this
1232+
coercion.
1233+
* If one of the fields is a partially applied constructor, we will eta-expand it
1234+
(this likely never happens).

src/meta/expr.lean

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,24 @@ meta def pi_arity_aux : ℕ → expr → ℕ
233233
meta def pi_arity : expr → ℕ :=
234234
pi_arity_aux 0
235235

236+
/-- Get the names of the bound variables by a sequence of pis or lambdas. -/
237+
meta def binding_names : expr → list name
238+
| (pi n _ _ e) := n :: e.binding_names
239+
| (lam n _ _ e) := n :: e.binding_names
240+
| e := []
241+
242+
/-- Instantiate lambdas in the second argument by expressions from the first. -/
243+
meta def instantiate_lambdas : list expr → expr → expr
244+
| (e'::es) (lam n bi t e) := instantiate_lambdas es (e.instantiate_var e')
245+
| _ e := e
246+
247+
/-- Instantiate lambdas in the second argument `e` by expressions from the first argument `es`.
248+
If the length of `es` is larger than the number of lambdas in `e`,
249+
then the term is applied to the remaining terms -/
250+
meta def instantiate_lambdas_or_apps : list expr → expr → expr
251+
| (e'::es) (lam n bi t e) := instantiate_lambdas_or_apps es (e.instantiate_var e')
252+
| es e := mk_app e es
253+
236254
end expr
237255

238256
namespace environment
@@ -264,6 +282,17 @@ option.is_some $ do
264282
(some di.type) | none,
265283
env.is_projection (n ++ x.deinternalize_field)
266284

285+
/-- Get all projections of the structure `n`. Returns `none` if `n` is not structure-like.
286+
If `n` is not a structure, but is structure-like, this does not check whether the names
287+
are existing declarations. -/
288+
meta def get_projections (env : environment) (n : name) : option (list name) := do
289+
(nparams, intro) ← env.is_structure_like n,
290+
di ← (env.get intro).to_option,
291+
tgt ← nparams.iterate
292+
(λ e : option expr, do expr.pi _ _ _ body ← e | none, some body)
293+
(some di.type) | none,
294+
return $ tgt.binding_names.map (λ x, n ++ x.deinternalize_field)
295+
267296
/-- For all declarations `d` where `f d = some x` this adds `x` to the returned list. -/
268297
meta def decl_filter_map {α : Type} (e : environment) (f : declaration → option α) : list α :=
269298
e.fold [] $ λ d l, match f d with
@@ -303,6 +332,60 @@ s.is_prefix_of $ (e.decl_olean n).get_or_else ""
303332

304333
end environment
305334

335+
336+
namespace expr
337+
/- In this section we define the tactic `is_eta_expansion` which checks whether an expression
338+
is an eta-expansion of a structure. (not to be confused with eta-expanion for `λ`). -/
339+
open tactic
340+
341+
/-- Checks whether for all elements `(nm, val)` in the list we have `val = nm.{univs} args` -/
342+
meta def is_eta_expansion_of (args : list expr) (univs : list level) (l : list (name × expr)) :
343+
bool :=
344+
l.all $ λ⟨proj, val⟩, val = (const proj univs).mk_app args
345+
346+
/-- Checks whether there is an expression `e` such that for all elements `(nm, val)` in the list
347+
`val = nm ... e` where `...` denotes the same list of parameters for all applications.
348+
Returns e if it exists. -/
349+
meta def is_eta_expansion_test : list (name × expr) → option expr
350+
| [] := none
351+
| (⟨proj, val⟩::l) :=
352+
match val.get_app_fn with
353+
| (const nm univs : expr) :=
354+
if nm = proj then
355+
let args := val.get_app_args in
356+
let e := args.ilast in
357+
if is_eta_expansion_of args univs l then some e else none
358+
else
359+
none
360+
| _ := none
361+
end
362+
363+
/-- Checks whether there is an expression `e` such that for all *non-propositional* elements
364+
`(nm, val)` in the list `val = e ... nm` where `...` denotes the same list of parameters for all
365+
applications. Also checks whether the resulting expression unifies with the given one -/
366+
meta def is_eta_expansion_aux (val : expr) (l : list (name × expr)) : tactic (option expr) :=
367+
do l' ← l.mfilter (λ⟨proj, val⟩, bnot <$> is_proof val),
368+
match is_eta_expansion_test l' with
369+
| some e := option.map (λ _, e) <$> try_core (unify e val)
370+
| none := return none
371+
end
372+
373+
/-- Checks whether there is an expression `e` such that `val` is the eta-expansion of `e`.
374+
This assumes that `val` is a fully-applied application of the constructor of a structure.
375+
376+
This is useful to reduce expressions generated by the notation
377+
`{ field_1 := _, ..other_structure }`
378+
If `other_structure` is itself a field of the structure, then the elaborator will insert an
379+
eta-expanded version of `other_structure`. -/
380+
meta def is_eta_expansion (val : expr) : tactic (option expr) := do
381+
e ← get_env,
382+
type ← infer_type val,
383+
projs ← e.get_projections type.get_app_fn.const_name,
384+
let args := (val.get_app_args).drop type.get_app_args.length,
385+
is_eta_expansion_aux val (projs.zip args)
386+
387+
end expr
388+
306389
namespace declaration
307390
open tactic
308391

src/tactic/basic.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import
1818
tactic.rewrite
1919
tactic.sanity_check
2020
tactic.simpa
21+
tactic.simps
2122
tactic.split_ifs
2223
tactic.squeeze
2324
tactic.well_founded_tactics

src/tactic/core.lean

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,16 @@ do e ← tactic.get_env,
125125
then s.insert d.to_name d else s),
126126
pure xs
127127

128+
/-- If `{nm}_{n}` doesn't exist in the environment, returns that, otherwise tries `{nm}_{n+1}` -/
129+
meta def get_unused_decl_name_aux (e : environment) (nm : name) : ℕ → tactic name | n :=
130+
let nm' := nm.append_suffix ("_" ++ to_string n) in
131+
if e.contains nm' then get_unused_decl_name_aux (n+1) else return nm'
132+
133+
/-- Return a name which doesn't already exist in the environment. If `nm` doesn't exist, it
134+
returns that, otherwise it tries nm_2, nm_3, ... -/
135+
meta def get_unused_decl_name (nm : name) : tactic name :=
136+
get_env >>= λ e, if e.contains nm then get_unused_decl_name_aux e nm 2 else return nm
137+
128138
/--
129139
Returns a pair (e, t), where `e ← mk_const d.to_name`, and `t = d.type`
130140
but with universe params updated to match the fresh universe metavariables in `e`.

src/tactic/default.lean

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,4 @@ import
3030
tactic.wlog
3131
tactic.tfae
3232
tactic.apply_fun
33-
tactic.localized
3433
tactic.apply

src/tactic/simps.lean

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/-
2+
Copyright (c) 2019 Floris van Doorn. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Authors: Floris van Doorn
5+
-/
6+
import tactic.core
7+
/-!
8+
# simps attribute
9+
This file defines the `@[simps]` attribute, to automatically generate simp-lemmas
10+
reducing a definition when projections are applied to it.
11+
12+
## Tags
13+
structures, projections, simp, simplifier, generates declarations
14+
-/
15+
16+
open tactic expr
17+
18+
/-- Add a lemma with `nm` stating that `lhs = rhs`. `type` is the type of both `lhs` and `rhs`,
19+
`args` is the list of local constants occurring, and `univs` is the list of universe variables.
20+
If `add_simp` then we make the resulting lemma a simp-lemma. -/
21+
meta def add_projection (nm : name) (type lhs rhs : expr) (args : list expr)
22+
(univs : list name) (add_simp : bool) : tactic unit := do
23+
eq_ap ← mk_mapp `eq $ [type, lhs, rhs].map some,
24+
refl_ap ← mk_app `eq.refl [type, lhs],
25+
decl_name ← get_unused_decl_name nm,
26+
let decl_type := eq_ap.pis args,
27+
let decl_value := refl_ap.lambdas args,
28+
let decl := declaration.thm decl_name univs decl_type (pure decl_value),
29+
add_decl decl <|> fail format!"failed to add projection lemma {decl_name}.",
30+
when add_simp $
31+
set_basic_attribute `simp decl_name tt >> set_basic_attribute `_refl_lemma decl_name tt
32+
33+
/-- Derive lemmas specifying the projections of the declaration. -/
34+
meta def add_projections : ∀(e : environment) (nm : name) (type lhs rhs : expr)
35+
(args : list expr) (univs : list name) (add_simp must_be_str : bool), tactic unit
36+
| e nm type lhs rhs args univs add_simp must_be_str := do
37+
(type_args, tgt) ← mk_local_pis_whnf type,
38+
tgt ← whnf tgt,
39+
let new_args := args ++ type_args,
40+
let lhs_ap := lhs.mk_app type_args,
41+
let rhs_ap := rhs.instantiate_lambdas_or_apps type_args,
42+
let str := tgt.get_app_fn.const_name,
43+
if e.is_structure str then do
44+
projs ← e.get_projections str,
45+
[intro] ← return $ e.constructors_of str | fail "unreachable code (3)",
46+
let params := get_app_args tgt, -- the parameters of the structure
47+
if is_constant_of (get_app_fn rhs_ap) intro then do -- if the value is a constructor application
48+
guard ((get_app_args rhs_ap).take params.length = params) <|> fail "unreachable code (1)",
49+
let rhs_args := (get_app_args rhs_ap).drop params.length, -- the fields of the structure
50+
guard (rhs_args.length = projs.length) <|> fail "unreachable code (2)",
51+
let pairs := projs.zip rhs_args,
52+
eta ← expr.is_eta_expansion_aux rhs_ap pairs,
53+
match eta with
54+
| none :=
55+
pairs.mmap' $ λ ⟨proj, new_rhs⟩, do
56+
new_type ← infer_type new_rhs,
57+
b ← is_prop new_type,
58+
when ¬ b $ do -- if this field is a proposition, we skip it
59+
-- cannot use `mk_app` here, since the resulting application might still be a function.
60+
new_lhs ← mk_mapp proj $ (params ++ [lhs_ap]).map some,
61+
let new_nm := nm.append_suffix $ "_" ++ proj.last,
62+
add_projections e new_nm new_type new_lhs new_rhs new_args univs add_simp ff
63+
| (some eta_reduction) := add_projection nm tgt lhs_ap eta_reduction new_args univs add_simp
64+
end
65+
else (do
66+
when must_be_str $
67+
fail "Invalid `simps` attribute. Body is not a constructor application",
68+
add_projection nm tgt lhs_ap rhs_ap new_args univs add_simp)
69+
else
70+
(do when must_be_str $
71+
fail "Invalid `simps` attribute. Target is not a structure",
72+
add_projection nm tgt lhs_ap rhs_ap new_args univs add_simp)
73+
74+
/-- Derive lemmas specifying the projections of the declaration. -/
75+
meta def simps_tac (nm : name) (add_simp : bool) : tactic unit := do
76+
e ← get_env,
77+
d ← e.get nm,
78+
let lhs : expr := const d.to_name (d.univ_params.map level.param),
79+
add_projections e nm d.type lhs d.value [] d.univ_params add_simp tt
80+
81+
reserve notation `lemmas_only`
82+
setup_tactic_parser
83+
84+
/-- Automatically derive lemmas specifying the projections of this declaration.
85+
Example: (note that the forward and reverse functions are specified differently!)
86+
```
87+
@[simps] def refl (α) : α ≃ α := ⟨id, λ x, x, λ x, rfl, λ x, rfl⟩
88+
```
89+
derives two simp-lemmas:
90+
```
91+
@[simp] lemma refl_to_fun (α) (x : α) : (refl α).to_fun x = id x
92+
@[simp] lemma refl_inv_fun (α) (x : α) : (refl α).inv_fun x = x
93+
```
94+
* It does not derive simp-lemmas for the prop-valued projections.
95+
* It will automatically reduce newly created beta-redexes, but not unfold any definitions.
96+
* If one of the fields itself is a structure, this command will recursively create
97+
simp-lemmas for all fields in that structure.
98+
* If one of the values is an eta-expanded structure, we will eta-reduce this structure.
99+
* You can use `@[simps lemmas_only]` to derive the lemmas, but not mark them
100+
as simp-lemmas.
101+
* If one of the projections is marked as a coercion, the generated lemmas do *not* use this
102+
coercion.
103+
* If one of the fields is a partially applied constructor, we will eta-expand it
104+
(this likely never happens).
105+
-/
106+
@[user_attribute] meta def simps_attr : user_attribute unit (option unit) :=
107+
{ name := `simps,
108+
descr := "Automatically derive lemmas specifying the projections of this declaration.",
109+
parser := optional (tk "lemmas_only"),
110+
after_set := some $
111+
λ n _ _, option.is_none <$> simps_attr.get_param n >>= simps_tac n }

0 commit comments

Comments
 (0)