Skip to content

Commit 2de70d7

Browse files
committed
feat(Tactic): automatically replace convert with convert!
This PR sets up an option for `congr!` to close generated goals with configurable transparency, but does not enable this by default. Then it adds a check to `convert` where it will run the `congr!` algorithm at both reducible and default transparency. In the case the default transparency generates different goals, it pops up a "Try this:" suggestion to use `convert!`. Applying all the "Try this:" suggestions to Mathlib means we can then change the semantics of `convert` to close the goals reducibly. Note that `convert` and `convert!` still do the same thing and result in the same goals after this PR is applied, except `convert!` does not pop up a "Try this:" afterwards.
1 parent 7111187 commit 2de70d7

3 files changed

Lines changed: 118 additions & 23 deletions

File tree

Mathlib/Tactic/CongrExclamation.lean

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ to right-hand sides of goals. Here is an exhaustive list of things it can try:
6363
6464
- It can try to close goals using a few strategies, including checking
6565
definitional equality, trying to apply `Subsingleton.elim` or `proof_irrel_heq`, and using the
66-
`assumption` tactic.
66+
`assumption` tactic. Discharging is done at default transparency (but this will be changed
67+
to reducible transparency in the future).
6768
6869
The optional parameter is the depth of the recursive applications.
6970
This is useful when `congr!` is too aggressive in breaking down the goal.
@@ -106,7 +107,8 @@ structure Congr!.Config where
106107
closePre : Bool := true
107108
/-- If `closePost := true`, then try to close goals that remain after no more congruence
108109
lemmas can be applied, using the same tactics as `closePre`. These tactics are applied
109-
with current tactic transparency level. -/
110+
with the transparency level specified by `postTransparency`, which is currently set to
111+
`.default` but will soon become `.reducible`. -/
110112
closePost : Bool := true
111113
/-- The transparency level to use when applying a congruence theorem.
112114
By default this is `.reducible`, which prevents unfolding of most definitions. -/
@@ -115,6 +117,11 @@ structure Congr!.Config where
115117
This includes trying to prove the goal by `rfl` and using the `assumption` tactic.
116118
By default this is `.reducible`, which prevents unfolding of most definitions. -/
117119
preTransparency : TransparencyMode := TransparencyMode.reducible
120+
/-- The transparency level to use when trying to close goals after no more congruence lemmas can
121+
be applied. This includes trying to prove the goal by `rfl` and using the `assumption` tactic.
122+
For backwards compatibility this is set to `.default`, which will be changed soon to `.reducible`.
123+
-/
124+
postTransparency : TransparencyMode := TransparencyMode.default
118125
/-- For passes that synthesize a congruence lemma using one side of the equality,
119126
we run the pass both for the left-hand side and the right-hand side. If `preferLHS` is `true`
120127
then we start with the left-hand side.
@@ -190,6 +197,7 @@ def Congr!.Config.unfoldSameFun : Congr!.Config where
190197
sameFun := true
191198
transparency := .default
192199
preTransparency := .default
200+
postTransparency := .default
193201

194202
/-- Whether the given number of arguments is allowed to be considered. -/
195203
def Congr!.Config.numArgsOk (config : Config) (numArgs : Nat) : Bool :=
@@ -636,7 +644,8 @@ where
636644
/-- Convert a goal into an `Eq` goal if possible (since we have a better shot at those).
637645
Also, if `tryClose := true`, then try to close the goal using an assumption, `Subsingleton.Elim`,
638646
or definitional equality. -/
639-
def Lean.MVarId.preCongr! (mvarId : MVarId) (tryClose : Bool) : MetaM (Option MVarId) := do
647+
def Lean.MVarId.preCongr! (mvarId : MVarId) (tryClose : Bool) :
648+
MetaM (Option MVarId) := do
640649
-- Next, turn `HEq` and `Iff` into `Eq`
641650
let mvarId ← mvarId.heqOfEq
642651
if tryClose then
@@ -676,7 +685,8 @@ def Lean.MVarId.congrCore! (config : Congr!.Config) (mvarId : MVarId) :
676685
return none
677686

678687
/-- A pass to clean up after `Lean.MVarId.preCongr!` and `Lean.MVarId.congrCore!`. -/
679-
def Lean.MVarId.postCongr! (config : Congr!.Config) (mvarId : MVarId) : MetaM (Option MVarId) := do
688+
def Lean.MVarId.postCongr! (config : Congr!.Config) (mvarId : MVarId) : MetaM (Option MVarId) :=
689+
withTransparency config.postTransparency do
680690
let some mvarId ← mvarId.preCongr! config.closePost | return none
681691
-- Convert `p = q` to `p ↔ q`, which is likely the more useful form:
682692
let mvarId ← mvarId.propext

Mathlib/Tactic/Convert.lean

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,25 @@ public meta section
7777

7878
open Lean Meta Elab Tactic
7979

80+
/-- Configuration for the `convert` family of tactics.
81+
This is `Congr!.Config` with different, less aggressive, defaults.
82+
-/
83+
structure Convert.Config extends Congr!.Config where
84+
postTransparency := .default -- Will be set to .reducible in a follow-up PR
85+
86+
/-- Elaborator for `Convert.Config` (which is equivalent to `Congr!.Config`
87+
but with different, less aggressive, defaults). -/
88+
declare_config_elab Convert.elabConfig Convert.Config
89+
90+
/--
91+
A configuration option that makes `convert` do the sorts of aggressive unfoldings that `congr`
92+
does while also similarly preventing `convert` from considering partial applications or congruences
93+
between different functions being applied.
94+
-/
95+
abbrev Convert.Config.unfoldSameFun : Convert.Config :=
96+
{ Congr!.Config.unfoldSameFun with
97+
postTransparency := .default } -- Will be set to .reducible in a follow-up PR
98+
8099
/--
81100
Close the goal `g` using `Eq.mp v e`,
82101
where `v` is a metavariable asserting that the type of `g` and `e` are equal.
@@ -132,8 +151,7 @@ pattern-matched, like `rintro` would, using the `with` keyword.
132151
See also `convert_to t`, where `t` specifies the expected type, instead of a proof term of type `t`.
133152
In other words, `convert_to t` works like `convert (?_ : t)`. Both tactics use the same options.
134153
135-
* `convert! e` uses default transparency when solving side goals. This is currently the same
136-
behaviour as `convert`, but `convert` will use reducible transparency in the future.
154+
* `convert! e` uses default transparency, rather than reducible, when solving side goals.
137155
* `convert ← e` creates equality goals in the opposite direction (with the goal type on the right).
138156
* `convert e using n`, where `n` is a positive numeral, controls the depth with which congruence is
139157
applied. For example, if the main goal is `⊢ Prime (n + n + 1)` and `e : Prime (2 * n + 1)`, then
@@ -156,6 +174,7 @@ example {n : ℕ} (e : Prime (2 * n + 1)) :
156174
ring
157175
158176
-- `convert` can fail where `exact` succeeds.
177+
def p (n : ℕ) := True
159178
example (h : p 0) : p 1 := by
160179
fail_if_success
161180
convert h -- fails, left-over goal 1 = 0
@@ -172,16 +191,16 @@ example (p q : Nat → Prop) (h : ∀ ε > 0, p ε) :
172191
sorry
173192
```
174193
-/
175-
syntax (name := convert) "convert" Lean.Parser.Tactic.optConfig " ←"? ppSpace term (" using " num)?
176-
(" with" (ppSpace colGt rintroPat)*)? : tactic
194+
syntax (name := convert) "convert" "!"? Lean.Parser.Tactic.optConfig " ←"? ppSpace term
195+
(" using " num)? (" with" (ppSpace colGt rintroPat)*)? : tactic
177196

178197
@[tactic_alt convert]
179198
syntax (name := convert!) "convert!" Lean.Parser.Tactic.optConfig " ←"? ppSpace term
180199
(" using " num)? (" with" (ppSpace colGt rintroPat)*)? : tactic
181200

182201
macro_rules
183202
| `(tactic| convert! $cfg $[←%$l]? $t $[using $n]? $[with $[$w]*]?) =>
184-
`(tactic| convert $cfg $[←%$l]? $t:term $[using $n]? $[with $[$w]*]?)
203+
`(tactic| convert ! $cfg $[←%$l]? $t:term $[using $n]? $[with $[$w]*]?)
185204

186205
/--
187206
Elaborates `term` ensuring the expected type, allowing stuck metavariables.
@@ -200,14 +219,39 @@ def elabTermForConvert (term : Syntax) (expectedType? : Option Expr) :
200219
return t
201220

202221
elab_rules : tactic
203-
| `(tactic| convert $cfg $[←%$sym]? $term $[using $n]? $[with $ps?*]?) =>
222+
| `(tactic| convert%$tk $[!%$semireducible]? $cfg $[←%$sym]? $term $[using $n]? $[with $ps?*]?) =>
204223
withMainContext do
205-
let config ← Congr!.elabConfig (mkOptionalNode cfg)
224+
let config := { ← Convert.elabConfig cfg with }
206225
let patterns := (ps?.getD #[]).toList
207226
let expectedType ← mkFreshExprMVar (mkSort (← getLevel (← getMainTarget)))
208227
let (e, gs) ← elabTermForConvert term expectedType
209-
liftMetaTactic fun g ↦
210-
return (← g.convert e sym.isSome (n.map (·.getNat)) config patterns) ++ gs
228+
if semireducible.isNone then
229+
liftMetaTactic fun g ↦ do
230+
-- Suggest `convert!` instead of `convert` if we rely on default transparency.
231+
let redGoals ← g.convert e sym.isSome (n.map (·.getNat))
232+
{ config with postTransparency := .reducible }
233+
patterns
234+
let defaultGoals ← g.convert e sym.isSome (n.map (·.getNat))
235+
{ config with postTransparency := .default }
236+
patterns
237+
if redGoals.length == defaultGoals.length then
238+
let sameGoals ← try
239+
(redGoals.zip defaultGoals).allM fun (g₁, g₂) => do
240+
-- Check that they agree on the set of free variables, otherwise we get errors.
241+
-- We assume the context in the `convert` case is a subset of the `convert!` case
242+
-- since `convert!` can more agressively unfold and introduce more variables.
243+
if !(← g₁.getDecl).lctx.isSubPrefixOf (← g₂.getDecl).lctx then return false
244+
g₂.withContext <| withReducible <| isDefEq (← g₁.getType) (← g₂.getType)
245+
catch _ => pure false
246+
-- If the goals are not the same then we should preserve the original behaviour
247+
-- and use `convert!`.
248+
if !sameGoals then
249+
let tac ← `(tactic| convert!%$tk $cfg $[←%$sym]? $term $[using $n]? $[with $ps?*]?)
250+
TryThis.addSuggestion tk { suggestion := tac } (origSpan? := ← getRef)
251+
return defaultGoals ++ gs
252+
else
253+
liftMetaTactic fun g ↦
254+
return (← g.convert e sym.isSome (n.map (·.getNat)) config patterns) ++ gs
211255

212256
/--
213257
`convert_to t` on a goal `⊢ t'` changes the goal to `⊢ t` and adds new goals for proving the
@@ -221,8 +265,7 @@ pattern-matched, like `rintro` would, using the `with` keyword.
221265
`convert e`, where `e` is a term of type `t`, uses `e` to close the new main goal. In other words,
222266
`convert e` works like `convert_to t; refine e`. Both tactics use the same options.
223267
224-
* `convert_to! t` uses default transparency when solving side goals. This is currently the same
225-
behaviour as `convert_to`, but `convert_to` will use reducible transparency in the future.
268+
* `convert_to! t` uses default transparency, rather than reducible, when solving side goals.
226269
* `convert_to ty at h` changes the type of the local hypothesis `h` to `ty`. If later local
227270
hypotheses or the goal depend on `h`, then `convert_to t at h` may leave a copy of `h`.
228271
* `convert_to ← t` creates equality goals in the opposite direction (with the original goal type on
@@ -238,7 +281,7 @@ pattern-matched, like `rintro` would, using the `with` keyword.
238281
* `convert_to (config := cfg) t` uses the configuration options in `cfg` to control the congruence
239282
rules (see `Congr!.Config`).
240283
-/
241-
syntax (name := convertTo) "convert_to" Lean.Parser.Tactic.optConfig " ←"? ppSpace term
284+
syntax (name := convertTo) "convert_to" ("!")? Parser.Tactic.optConfig " ←"? ppSpace term
242285
(" using " num)? (" with" (ppSpace colGt rintroPat)*)? (Parser.Tactic.location)? : tactic
243286

244287
@[tactic_alt convertTo]
@@ -247,13 +290,20 @@ syntax (name := convert_to!) "convert_to!" Lean.Parser.Tactic.optConfig " ←"?
247290

248291
macro_rules
249292
| `(tactic| convert_to! $cfg $[←%$l]? $t $[using $n]? $[with $w]? $[$loc]?) =>
250-
`(tactic| convert_to $cfg $[←%$l]? $t:term $[using $n]? $[with $w]? $[$loc]?)
293+
`(tactic| convert_to ! $cfg $[←%$l]? $t:term $[using $n]? $[with $w]? $[$loc]?)
251294

252295
elab_rules : tactic
253-
| `(tactic| convert_to $cfg $[←%$sym]? $newType $[using $n]?
296+
| `(tactic| convert_to $[!%$semireducible]? $cfg $[←%$sym]? $newType $[using $n]?
254297
$[with $ps?*]? $[$loc?:location]?) => do
255298
let n : ℕ := n |>.map (·.getNat) |>.getD 1
256-
let config ← Congr!.elabConfig cfg
299+
let mut config := { ← Convert.elabConfig cfg with }
300+
if semireducible.isSome then
301+
config := { config with
302+
-- TODO: also enable this in the future? (Not right now, for backwards compatibility).
303+
-- transparency := default,
304+
-- preTransparency := default,
305+
-- postTransparency := default -- Disabled while the port to `convert!` happens.
306+
}
257307
let patterns := (ps?.getD #[]).toList
258308
withLocation (expandOptLocation (mkOptionalNode loc?))
259309
(atLocal := fun fvarId ↦ do
@@ -279,8 +329,7 @@ into new goals, using the hole's name, if any, as the goal case name.
279329
Like `congr!`, `convert_to` introduces variables while applying congruence rules. These can be
280330
pattern-matched, like `rintro` would, using the `with` keyword.
281331
282-
* `ac_change! t` uses default transparency when solving side goals. This is currently the same
283-
behaviour as `ac_change`, but `ac_change` will use reducible transparency in the future.
332+
* `ac_change! t` uses default transparency, rather than reducible, when solving side goals.
284333
* `ac_change t using n`, where `n` is a positive numeral, controls the depth with which congruence
285334
is applied. For example, if the main goal is `⊢ Prime ((a * b + 1) + c)`,
286335
then `ac_change Prime ((1 + a * b) + c) using 2` solves the side goals, and
@@ -305,5 +354,4 @@ macro_rules
305354
| `(tactic| ac_change! $t $[using $n]?) =>
306355
`(tactic| convert_to! $t:term $[using $n]? <;> try ac_rfl)
307356

308-
309357
end Mathlib.Tactic

MathlibTest/convert.lean

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ example (prime : Nat → Prop) (n : Nat) (h : prime (2 * n + 1)) :
7575

7676
example (p q : Nat → Prop) (h : ∀ ε > 0, p ε) :
7777
∀ ε > 0, q ε := by
78-
convert h using 2 with ε hε
78+
convert! h using 2 with ε hε
7979
guard_hyp hε : ε > 0
8080
guard_target = q ε ↔ p ε
8181
exact test_sorry
@@ -128,4 +128,41 @@ example (x y z : Nat) (h : x + y = z) : y + x = z := by
128128
· rw [Nat.add_comm]
129129
exact h
130130

131+
/-! Check that we don't unfold at semireducible transparency: although `congr!` (which
132+
`convert` relies on) applies lemmas at reducible transparency, it used to call
133+
`assumption`/`rfl` at default transparency and solve too much.
134+
135+
`convert!` uses default transparency throughout, and solves the goals all at once.
136+
-/
137+
138+
/-- An identity function at default transparency, to test that we don't unfold too much. -/
139+
def semireducibleId {α : Type*} (a : α) := a
140+
141+
/-
142+
-- These examples can be uncommented when `convert` is reducible by default.
143+
example (P : ℕ → Prop) {a b : ℕ} (h : P a) : P (semireducibleId a) := by
144+
convert h
145+
guard_target =ₛ semireducibleId a = a
146+
rfl
147+
148+
example (P : ℕ → Prop) {a b : ℕ} (h : P a) : P (semireducibleId a) := by
149+
convert! h
150+
151+
example (P : ℕ → Prop) {a b : ℕ} (hab : b = a) (h : P a) : P (semireducibleId b) := by
152+
convert h
153+
guard_target =ₛ semireducibleId b = a
154+
exact hab
155+
156+
example (P : ℕ → Prop) {a b : ℕ} (hab : b = a) (h : P (semireducibleId a)) : P b := by
157+
convert! h
158+
159+
example (P : ℕ → Prop) {a b : ℕ} (hab : b = a) (h : P a) : P (semireducibleId b) := by
160+
convert h
161+
guard_target =ₛ semireducibleId b = a
162+
exact hab
163+
164+
example (P : ℕ → Prop) {a b : ℕ} (hab : b = a) (h : P (semireducibleId a)) : P b := by
165+
convert! h
166+
-/
167+
131168
end Tests

0 commit comments

Comments
 (0)