Skip to content

Commit b1f215c

Browse files
committed
feat(Tactic): convert discharges side goals reducibly
This PR implements the main behaviour change from #39039: `convert` now discharges side goals at reducible transparency. `convert!`, which we previously switched all calls inside Mathlib to, keeps the old behaviour of working at default transparency. (Everything discussed in this PR holds analogously for the other tactics in the family: `convert_to` and `ac_change`). The main trick in this PR is in allowing `convert!` to have different configuration options than `convert`, but still allow the user to override those options too. So `convert! (postTransparency := .instances)` will have transparency set to `.instances`, not `.default`. We achieve this by having two new structures that copy `Congr!.Config` but set different default values, and choosing between the corresponding elaborators for those configs based on the presence of an `!`. I got this trick from Jovan's #38071 which also changes settings in `Convert.Config`. This PR should be mostly compatible with #38071 and I'd like to get both this one and #38071 in before wrapping up #39039 by replacing unnecessary calls to `convert!` with `convert`.
1 parent ddf19b2 commit b1f215c

3 files changed

Lines changed: 133 additions & 16 deletions

File tree

Mathlib/Tactic/CongrExclamation.lean

Lines changed: 13 additions & 3 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 (unless specified otherwise),
67+
but this will become 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 default
111+
transparency but in the future will become `.reducible` by default. -/
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,12 @@ 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 in the future to
123+
`.reducible`.
124+
-/
125+
postTransparency : TransparencyMode := TransparencyMode.default
118126
/-- For passes that synthesize a congruence lemma using one side of the equality,
119127
we run the pass both for the left-hand side and the right-hand side. If `preferLHS` is `true`
120128
then we start with the left-hand side.
@@ -190,6 +198,7 @@ def Congr!.Config.unfoldSameFun : Congr!.Config where
190198
sameFun := true
191199
transparency := .default
192200
preTransparency := .default
201+
postTransparency := .default
193202

194203
/-- Whether the given number of arguments is allowed to be considered. -/
195204
def Congr!.Config.numArgsOk (config : Config) (numArgs : Nat) : Bool :=
@@ -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: 79 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,75 @@ 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+
To elaborate config options for `convert`, use `Convert.elabConfig` which chooses
84+
between `Convert.CheapConfig` and `Convert.ExpensiveConfig` based on other flags.
85+
-/
86+
structure Convert.CheapConfig extends Congr!.Config where
87+
postTransparency := .reducible
88+
89+
/-- Internal elaborator for `Convert.CheapConfig`: use `Convert.elabConfig` instead. -/
90+
declare_config_elab Convert.elabCheapConfig Convert.CheapConfig
91+
92+
/-- A configuration option that makes `convert` do the sorts of aggressive unfoldings that `congr`
93+
does while also similarly preventing `convert` from considering partial applications or congruences
94+
between different functions being applied.
95+
96+
Note that `convert (config := .unfoldSameFun)` and `convert! (config := .unfoldSameFun)`
97+
currently do the same thing since `.unfoldSameFun` runs at default transparency always.
98+
This may change in the future, if `convert!` affects other options too.
99+
-/
100+
abbrev Convert.CheapConfig.unfoldSameFun : Convert.CheapConfig :=
101+
{ Congr!.Config.unfoldSameFun with }
102+
103+
/-- Configuration for the `convert!` family of tactics.
104+
This is `Convert.CheapConfig` (used by `convert` without exclamation mark) with different,
105+
more aggressive, defaults.
106+
107+
To elaborate config options for `convert`, use `Convert.elabConfig` which chooses
108+
between `Convert.CheapConfig` and `Convert.ExpensiveConfig` based on other flags.
109+
110+
We separate out the two structures to allow the user to explicitly set options in the call, so for
111+
example the following call runs at `.instances` transparency.
112+
```
113+
convert! (postTransparency := .instances)
114+
```
115+
-/
116+
structure Convert.ExpensiveConfig extends Convert.CheapConfig where
117+
-- TODO: also enable this in the future? (For backwards compatibility we won't do so right now.)
118+
-- preTransparency := .default
119+
-- transparency := .default
120+
postTransparency := .default
121+
122+
/-- Internal elaborator for `Convert.ExpensiveConfig`: use `Convert.elabConfig` instead. -/
123+
declare_config_elab Convert.elabExpensiveConfig Convert.ExpensiveConfig
124+
125+
/-- A configuration option that makes `convert!` do the sorts of aggressive unfoldings that `congr`
126+
does while also similarly preventing `convert!` from considering partial applications or congruences
127+
between different functions being applied.
128+
129+
Note that `convert (config := .unfoldSameFun)` and `convert! (config := .unfoldSameFun)`
130+
currently do the same thing since `.unfoldSameFun` runs at default transparency always.
131+
This may change in the future, if `convert!` affects other options too.
132+
-/
133+
abbrev Convert.ExpensiveConfig.unfoldSameFun : Convert.ExpensiveConfig :=
134+
{ Congr!.Config.unfoldSameFun with }
135+
136+
/-- Configuration elaborator for the `convert`/`convert!` family of tactics.
137+
138+
If `expensive` is true, we're elaborating for `convert!`, and will configure to run at default
139+
transparency (unless explicitly overridden by the user saying e.g. `(transparency := .instances)`.)
140+
-/
141+
def Convert.elabConfig (expensive : Bool) (stx : Syntax) : TacticM Congr!.Config := do
142+
-- Implement overridable fields by choosing to elaborate one of two structures,
143+
-- which have different defaults (that can later be overridden by the user).
144+
if expensive then
145+
pure { ← Convert.elabExpensiveConfig stx with }
146+
else
147+
pure { ← Convert.elabCheapConfig stx with }
148+
80149
/--
81150
Close the goal `g` using `Eq.mp v e`,
82151
where `v` is a metavariable asserting that the type of `g` and `e` are equal.
@@ -132,8 +201,7 @@ pattern-matched, like `rintro` would, using the `with` keyword.
132201
See also `convert_to t`, where `t` specifies the expected type, instead of a proof term of type `t`.
133202
In other words, `convert_to t` works like `convert (?_ : t)`. Both tactics use the same options.
134203
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.
204+
* `convert! e` uses default transparency, rather than reducible, when solving side goals.
137205
* `convert ← e` creates equality goals in the opposite direction (with the goal type on the right).
138206
* `convert e using n`, where `n` is a positive numeral, controls the depth with which congruence is
139207
applied. For example, if the main goal is `⊢ Prime (n + n + 1)` and `e : Prime (2 * n + 1)`, then
@@ -156,6 +224,7 @@ example {n : ℕ} (e : Prime (2 * n + 1)) :
156224
ring
157225
158226
-- `convert` can fail where `exact` succeeds.
227+
def p (n : ℕ) := True
159228
example (h : p 0) : p 1 := by
160229
fail_if_success
161230
convert h -- fails, left-over goal 1 = 0
@@ -200,9 +269,9 @@ def elabTermForConvert (term : Syntax) (expectedType? : Option Expr) :
200269
return t
201270

202271
elab_rules : tactic
203-
| `(tactic| convert $[!]? $cfg $[←%$sym]? $term $[using $n]? $[with $ps?*]?) =>
272+
| `(tactic| convert $[!%$expensive]? $cfg $[←%$sym]? $term $[using $n]? $[with $ps?*]?) =>
204273
withMainContext do
205-
let config ← Congr!.elabConfig (mkOptionalNode cfg)
274+
let config ← Convert.elabConfig expensive.isSome cfg
206275
let patterns := (ps?.getD #[]).toList
207276
let expectedType ← mkFreshExprMVar (mkSort (← getLevel (← getMainTarget)))
208277
let (e, gs) ← elabTermForConvert term expectedType
@@ -221,8 +290,7 @@ pattern-matched, like `rintro` would, using the `with` keyword.
221290
`convert e`, where `e` is a term of type `t`, uses `e` to close the new main goal. In other words,
222291
`convert e` works like `convert_to t; refine e`. Both tactics use the same options.
223292
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.
293+
* `convert_to! t` uses default transparency, rather than reducible, when solving side goals.
226294
* `convert_to ty at h` changes the type of the local hypothesis `h` to `ty`. If later local
227295
hypotheses or the goal depend on `h`, then `convert_to t at h` may leave a copy of `h`.
228296
* `convert_to ← t` creates equality goals in the opposite direction (with the original goal type on
@@ -238,7 +306,7 @@ pattern-matched, like `rintro` would, using the `with` keyword.
238306
* `convert_to (config := cfg) t` uses the configuration options in `cfg` to control the congruence
239307
rules (see `Congr!.Config`).
240308
-/
241-
syntax (name := convertTo) "convert_to" Lean.Parser.Tactic.optConfig " ←"? ppSpace term
309+
syntax (name := convertTo) "convert_to" ("!")? Lean.Parser.Tactic.optConfig " ←"? ppSpace term
242310
(" using " num)? (" with" (ppSpace colGt rintroPat)*)? (Parser.Tactic.location)? : tactic
243311

244312
@[tactic_alt convertTo]
@@ -247,13 +315,13 @@ syntax (name := convert_to!) "convert_to!" Lean.Parser.Tactic.optConfig " ←"?
247315

248316
macro_rules
249317
| `(tactic| convert_to! $cfg $[←%$l]? $t $[using $n]? $[with $w]? $[$loc]?) =>
250-
`(tactic| convert_to $cfg $[←%$l]? $t:term $[using $n]? $[with $w]? $[$loc]?)
318+
`(tactic| convert_to ! $cfg $[←%$l]? $t:term $[using $n]? $[with $w]? $[$loc]?)
251319

252320
elab_rules : tactic
253-
| `(tactic| convert_to $cfg $[←%$sym]? $newType $[using $n]?
321+
| `(tactic| convert_to $[!%$expensive]? $cfg $[←%$sym]? $newType $[using $n]?
254322
$[with $ps?*]? $[$loc?:location]?) => do
255323
let n : ℕ := n |>.map (·.getNat) |>.getD 1
256-
let config ← Congr!.elabConfig cfg
324+
let config ← Convert.elabConfig expensive.isSome cfg
257325
let patterns := (ps?.getD #[]).toList
258326
withLocation (expandOptLocation (mkOptionalNode loc?))
259327
(atLocal := fun fvarId ↦ do
@@ -279,8 +347,7 @@ into new goals, using the hole's name, if any, as the goal case name.
279347
Like `congr!`, `convert_to` introduces variables while applying congruence rules. These can be
280348
pattern-matched, like `rintro` would, using the `with` keyword.
281349
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.
350+
* `ac_change! t` uses default transparency, rather than reducible, when solving side goals.
284351
* `ac_change t using n`, where `n` is a positive numeral, controls the depth with which congruence
285352
is applied. For example, if the main goal is `⊢ Prime ((a * b + 1) + c)`,
286353
then `ac_change Prime ((1 + a * b) + c) using 2` solves the side goals, and
@@ -305,5 +372,4 @@ macro_rules
305372
| `(tactic| ac_change! $t $[using $n]?) =>
306373
`(tactic| convert_to! $t:term $[using $n]? <;> try ac_rfl)
307374

308-
309375
end Mathlib.Tactic

MathlibTest/Tactic/Convert/Basic.lean

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ example (prime : Nat → Prop) (n : Nat) (h : prime (2 * n + 1)) :
7373
guard_target = n + n = 2 * n
7474
exact test_sorry
7575

76+
-- `convert! (config := .unfoldSameFun)` does the same thing as `convert (config := .unfoldSameFun)`
77+
-- (at least for now)
78+
example (prime : Nat → Prop) (n : Nat) (h : prime (2 * n + 1)) :
79+
prime (n + n + 1) := by
80+
convert! (config := .unfoldSameFun) h
81+
guard_target = n + n = 2 * n
82+
exact test_sorry
83+
7684
example (p q : Nat → Prop) (h : ∀ ε > 0, p ε) :
7785
∀ ε > 0, q ε := by
7886
convert h using 2 with ε hε
@@ -128,4 +136,37 @@ example (x y z : Nat) (h : x + y = z) : y + x = z := by
128136
· rw [Nat.add_comm]
129137
exact h
130138

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

0 commit comments

Comments
 (0)