/
squeeze.lean
350 lines (303 loc) · 13.9 KB
/
squeeze.lean
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
/-
Copyright (c) 2019 Simon Hudon. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Author: Simon Hudon
-/
import control.traversable.basic
import tactic.simpa
open interactive interactive.types lean.parser
private meta def loc.to_string_aux : option name → string
| none := "⊢"
| (some x) := to_string x
/-- pretty print a `loc` -/
meta def loc.to_string : loc → string
| (loc.ns []) := ""
| (loc.ns [none]) := ""
| (loc.ns ls) := string.join $ list.intersperse " " (" at" :: ls.map loc.to_string_aux)
| loc.wildcard := " at *"
/-- shift `pos` `n` columns to the left -/
meta def pos.move_left (p : pos) (n : ℕ) : pos :=
{ line := p.line, column := p.column - n }
namespace tactic
open list
/-- parse structure instance of the shape `{ field1 := value1, .. , field2 := value2 }` -/
meta def struct_inst : lean.parser pexpr :=
do tk "{",
ls ← sep_by (skip_info (tk ","))
( sum.inl <$> (tk ".." *> texpr) <|>
sum.inr <$> (prod.mk <$> ident <* tk ":=" <*> texpr)),
tk "}",
let (srcs,fields) := partition_map id ls,
let (names,values) := unzip fields,
pure $ pexpr.mk_structure_instance
{ field_names := names,
field_values := values,
sources := srcs }
/-- pretty print structure instance -/
meta def struct.to_tactic_format (e : pexpr) : tactic format :=
do r ← e.get_structure_instance_info,
fs ← mzip_with (λ n v,
do v ← to_expr v >>= pp,
pure $ format!"{n} := {v}" )
r.field_names r.field_values,
let ss := r.sources.map (λ s, format!" .. {s}"),
let x : format := format.join $ list.intersperse ", " (fs ++ ss),
pure format!" {{{x}}"
/-- Attribute containing a table that accumulates multiple `squeeze_simp` suggestions -/
@[user_attribute]
private meta def squeeze_loc_attr : user_attribute unit (option (list (pos × string × list simp_arg_type × string))) :=
{ name := `_squeeze_loc,
parser := fail "this attribute should not be used",
descr := "table to accumulate multiple `squeeze_simp` suggestions" }
/-- dummy declaration used as target of `squeeze_loc` attribute -/
def squeeze_loc_attr_carrier := ()
run_cmd squeeze_loc_attr.set ``squeeze_loc_attr_carrier none tt
/-- Format a list of arguments for use with `simp` and friends. This omits the
list entirely if it is empty. -/
meta def render_simp_arg_list : list simp_arg_type → tactic format
| [] := pure ""
| args := (++) " " <$> to_line_wrap_format <$> args.mmap pp
/-- Emit a suggestion to the user. If inside a `squeeze_scope` block,
the suggestions emitted through `mk_suggestion` will be aggregated so that
every tactic that makes a suggestion can consider multiple execution of the
same invocation.
If `at_pos` is true, make the suggestion at `p` instead of the current position. -/
meta def mk_suggestion (p : pos) (pre post : string) (args : list simp_arg_type)
(at_pos := ff) : tactic unit :=
do xs ← squeeze_loc_attr.get_param ``squeeze_loc_attr_carrier,
match xs with
| none := do
args ← render_simp_arg_list args,
if at_pos then
@scope_trace _ p.line p.column $ λ _, _root_.trace sformat!"{pre}{args}{post}" (pure () : tactic unit)
else
trace sformat!"{pre}{args}{post}"
| some xs := do
squeeze_loc_attr.set ``squeeze_loc_attr_carrier ((p,pre,args,post) :: xs) ff
end
local postfix `?`:9001 := optional
/-- translate a `pexpr` into a `simp` configuration -/
meta def parse_config : option pexpr → tactic (simp_config_ext × format)
| none := pure ({}, "")
| (some cfg) :=
do e ← to_expr ``(%%cfg : simp_config_ext),
fmt ← has_to_tactic_format.to_tactic_format cfg,
prod.mk <$> eval_expr simp_config_ext e
<*> struct.to_tactic_format cfg
/-- translate a `pexpr` into a `dsimp` configuration -/
meta def parse_dsimp_config : option pexpr → tactic (dsimp_config × format)
| none := pure ({}, "")
| (some cfg) :=
do e ← to_expr ``(%%cfg : simp_config_ext),
fmt ← has_to_tactic_format.to_tactic_format cfg,
prod.mk <$> eval_expr dsimp_config e
<*> struct.to_tactic_format cfg
/-- `same_result proof tac` runs tactic `tac` and checks if the proof
produced by `tac` is equivalent to `proof`. -/
meta def same_result (pr : proof_state) (tac : tactic unit) : tactic bool :=
do s ← get_proof_state_after tac,
pure $ some pr = s
private meta def filter_simp_set_aux
(tac : bool → list simp_arg_type → tactic unit)
(args : list simp_arg_type) (pr : proof_state) :
list simp_arg_type → list simp_arg_type →
list simp_arg_type → tactic (list simp_arg_type × list simp_arg_type)
| [] ys ds := pure (ys.reverse, ds.reverse)
| (x :: xs) ys ds :=
do b ← same_result pr (tac tt (args ++ xs ++ ys)),
if b
then filter_simp_set_aux xs ys (x:: ds)
else filter_simp_set_aux xs (x :: ys) ds
declare_trace squeeze.deleted
/--
`filter_simp_set g call_simp user_args simp_args` returns `args'` such that, when calling
`call_simp tt /- only -/ args'` on the goal `g` (`g` is a meta var) we end up in the same
state as if we had called `call_simp ff (user_args ++ simp_args)` and removing any one
element of `args'` changes the resulting proof.
-/
meta def filter_simp_set
(tac : bool → list simp_arg_type → tactic unit)
(user_args simp_args : list simp_arg_type) : tactic (list simp_arg_type) :=
do some s ← get_proof_state_after (tac ff (user_args ++ simp_args)),
(simp_args', _) ← filter_simp_set_aux tac user_args s simp_args [] [],
(user_args', ds) ← filter_simp_set_aux tac simp_args' s user_args [] [],
when (is_trace_enabled_for `squeeze.deleted = tt ∧ ¬ ds.empty)
trace!"deleting provided arguments {ds}",
pure (user_args' ++ simp_args')
/-- make a `simp_arg_type` that references the name given as an argument -/
meta def name.to_simp_args (n : name) : tactic simp_arg_type :=
do e ← resolve_name' n, pure $ simp_arg_type.expr e
/-- tactic combinator to create a `simp`-like tactic that minimizes its
argument list.
* `slow`: adds all rfl-lemmas from the environment to the initial list (this is a slower but more accurate strategy)
* `no_dflt`: did the user use the `only` keyword?
* `args`: list of `simp` arguments
* `tac`: how to invoke the underlying `simp` tactic
-/
meta def squeeze_simp_core
(slow no_dflt : bool) (args : list simp_arg_type)
(tac : Π (no_dflt : bool) (args : list simp_arg_type), tactic unit)
(mk_suggestion : list simp_arg_type → tactic unit) : tactic unit :=
do v ← target >>= mk_meta_var,
args ← if slow then do
simp_set ← attribute.get_instances `simp,
simp_set ← simp_set.mfilter $ has_attribute' `_refl_lemma,
simp_set ← simp_set.mmap $ resolve_name' >=> pure ∘ simp_arg_type.expr,
pure $ args ++ simp_set
else pure args,
g ← retrieve $ do
{ g ← main_goal,
tac no_dflt args,
instantiate_mvars g },
let vs := g.list_constant,
vs ← vs.mfilter is_simp_lemma,
vs ← vs.mmap strip_prefix,
vs ← vs.to_list.mmap name.to_simp_args,
with_local_goals' [v] (filter_simp_set tac args vs)
>>= mk_suggestion,
tac no_dflt args
namespace interactive
attribute [derive decidable_eq] simp_arg_type
/-- combinator meant to aggregate the suggestions issued by multiple calls
of `squeeze_simp` (due, for instance, to `;`).
Can be used as:
```lean
example {α β} (xs ys : list α) (f : α → β) :
(xs ++ ys.tail).map f = xs.map f ∧ (xs.tail.map f).length = xs.length :=
begin
have : xs = ys, admit,
squeeze_scope
{ split; squeeze_simp, -- `squeeze_simp` is run twice, the first one requires
-- `list.map_append` and the second one `[list.length_map, list.length_tail]`
-- prints only one message and combine the suggestions:
-- > Try this: simp only [list.length_map, list.length_tail, list.map_append]
squeeze_simp [this] -- `squeeze_simp` is run only once
-- prints:
-- > Try this: simp only [this]
},
end
```
-/
meta def squeeze_scope (tac : itactic) : tactic unit :=
do none ← squeeze_loc_attr.get_param ``squeeze_loc_attr_carrier | pure (),
squeeze_loc_attr.set ``squeeze_loc_attr_carrier (some []) ff,
finally tac $ do
some xs ← squeeze_loc_attr.get_param ``squeeze_loc_attr_carrier | fail "invalid state",
let m := native.rb_lmap.of_list xs,
squeeze_loc_attr.set ``squeeze_loc_attr_carrier none ff,
m.to_list.reverse.mmap' $ λ ⟨p,suggs⟩, do
{ let ⟨pre,_,post⟩ := suggs.head,
let suggs : list (list simp_arg_type) := suggs.map $ prod.fst ∘ prod.snd,
mk_suggestion p pre post (suggs.foldl list.union []) tt, pure () }
/--
`squeeze_simp`, `squeeze_simpa` and `squeeze_dsimp` perform the same
task with the difference that `squeeze_simp` relates to `simp` while
`squeeze_simpa` relates to `simpa` and `squeeze_dsimp` relates to
`dsimp`. The following applies to `squeeze_simp`, `squeeze_simpa` and
`squeeze_dsimp`.
`squeeze_simp` behaves like `simp` (including all its arguments)
and prints a `simp only` invocation to skip the search through the
`simp` lemma list.
For instance, the following is easily solved with `simp`:
```lean
example : 0 + 1 = 1 + 0 := by simp
```
To guide the proof search and speed it up, we may replace `simp`
with `squeeze_simp`:
```lean
example : 0 + 1 = 1 + 0 := by squeeze_simp
-- prints:
-- Try this: simp only [add_zero, eq_self_iff_true, zero_add]
```
`squeeze_simp` suggests a replacement which we can use instead of
`squeeze_simp`.
```lean
example : 0 + 1 = 1 + 0 := by simp only [add_zero, eq_self_iff_true, zero_add]
```
`squeeze_simp only` prints nothing as it already skips the `simp` list.
This tactic is useful for speeding up the compilation of a complete file.
Steps:
1. search and replace ` simp` with ` squeeze_simp` (the space helps avoid the
replacement of `simp` in `@[simp]`) throughout the file.
2. Starting at the beginning of the file, go to each printout in turn, copy
the suggestion in place of `squeeze_simp`.
3. after all the suggestions were applied, search and replace `squeeze_simp` with
`simp` to remove the occurrences of `squeeze_simp` that did not produce a suggestion.
Known limitation(s):
* in cases where `squeeze_simp` is used after a `;` (e.g. `cases x; squeeze_simp`),
`squeeze_simp` will produce as many suggestions as the number of goals it is applied to.
It is likely that none of the suggestion is a good replacement but they can all be
combined by concatenating their list of lemmas. `squeeze_scope` can be used to
combine the suggestions: `by squeeze_scope { cases x; squeeze_simp }`
* sometimes, `simp` lemmas are also `_refl_lemma` and they can be used without appearing in the
resulting proof. `squeeze_simp` won't know to try that lemma unless it is called as `squeeze_simp?`
-/
meta def squeeze_simp
(key : parse cur_pos)
(slow_and_accurate : parse (tk "?")?)
(use_iota_eqn : parse (tk "!")?) (no_dflt : parse only_flag) (hs : parse simp_arg_list)
(attr_names : parse with_ident_list) (locat : parse location)
(cfg : parse struct_inst?) : tactic unit :=
do (cfg',c) ← parse_config cfg,
squeeze_simp_core slow_and_accurate.is_some no_dflt hs
(λ l_no_dft l_args, simp use_iota_eqn l_no_dft l_args attr_names locat cfg')
(λ args,
let use_iota_eqn := if use_iota_eqn.is_some then "!" else "",
attrs := if attr_names.empty then "" else string.join (list.intersperse " " (" with" :: attr_names.map to_string)),
loc := loc.to_string locat in
mk_suggestion (key.move_left 1)
sformat!"Try this: simp{use_iota_eqn} only"
sformat!"{attrs}{loc}{c}" args)
/-- see `squeeze_simp` -/
meta def squeeze_simpa
(key : parse cur_pos)
(slow_and_accurate : parse (tk "?")?)
(use_iota_eqn : parse (tk "!")?) (no_dflt : parse only_flag) (hs : parse simp_arg_list)
(attr_names : parse with_ident_list) (tgt : parse (tk "using" *> texpr)?)
(cfg : parse struct_inst?) : tactic unit :=
do (cfg',c) ← parse_config cfg,
tgt' ← traverse (λ t, do t ← to_expr t >>= pp,
pure format!" using {t}") tgt,
squeeze_simp_core slow_and_accurate.is_some no_dflt hs
(λ l_no_dft l_args, simpa use_iota_eqn l_no_dft l_args attr_names tgt cfg')
(λ args,
let use_iota_eqn := if use_iota_eqn.is_some then "!" else "",
attrs := if attr_names.empty then "" else string.join (list.intersperse " " (" with" :: attr_names.map to_string)),
tgt' := tgt'.get_or_else "" in
mk_suggestion (key.move_left 1)
sformat!"Try this: simpa{use_iota_eqn} only"
sformat!"{attrs}{tgt'}{c}" args)
/-- `squeeze_dsimp` behaves like `dsimp` (including all its arguments)
and prints a `dsimp only` invocation to skip the search through the
`simp` lemma list. See the doc string of `squeeze_simp` for examples.
-/
meta def squeeze_dsimp
(key : parse cur_pos)
(slow_and_accurate : parse (tk "?")?)
(use_iota_eqn : parse (tk "!")?)
(no_dflt : parse only_flag) (hs : parse simp_arg_list)
(attr_names : parse with_ident_list) (locat : parse location)
(cfg : parse struct_inst?) : tactic unit :=
do (cfg',c) ← parse_dsimp_config cfg,
squeeze_simp_core slow_and_accurate.is_some no_dflt hs
(λ l_no_dft l_args, dsimp l_no_dft l_args attr_names locat cfg')
(λ args,
let use_iota_eqn := if use_iota_eqn.is_some then "!" else "",
attrs := if attr_names.empty then "" else string.join (list.intersperse " " (" with" :: attr_names.map to_string)),
loc := loc.to_string locat in
mk_suggestion (key.move_left 1)
sformat!"Try this: dsimp{use_iota_eqn} only"
sformat!"{attrs}{loc}{c}" args)
end interactive
end tactic
open tactic.interactive
add_tactic_doc
{ name := "squeeze_simp / squeeze_simpa / squeeze_dsimp / squeeze_scope",
category := doc_category.tactic,
decl_names :=
[``squeeze_simp,
``squeeze_dsimp,
``squeeze_simpa,
``squeeze_scope],
tags := ["simplification", "Try this"],
inherit_description_from := ``squeeze_simp }