Skip to content

Commit 267b8ed

Browse files
kmillalgebraic-dev
authored andcommitted
fix: refine how simp tracks unfolded local definitions (leanprover#8753)
This PR fixes a bug in `simp` where it was not resetting the set of zeta-delta reduced let definitions between `simp` calls. It also fixes a bug where `simp` would report zeta-delta reduced let definitions that weren't given as simp arguments (these extraneous let definitions appear due to certain processes temporarily setting `zetaDelta := true`). This PR also modifies the metaprogramming interface for the zeta-delta tracking functions to be re-entrant and to prevent this kind of no-reset bug from occurring again. Closes leanprover#6655. Re-entrance of this metaprogramming interface is not needed to fix leanprover#6655, but it is needed for some future PRs. The `tests/lean/run/6655.lean` file has an example of a deficiency of `simp?`, where `simp?` still over-reports unfolded let declarations. This is likely due to `withInferTypeConfig` setting `zetaDelta := true` from within `isDefEq`, but I did not verify this. This PR supersedes leanprover#7539. The difference is that this PR has `withResetZetaDeltaFVarIds` save and restore `zetaDeltaFVarIds`, but that PR saves and then extends `zetaDeltaFVarIds` to persist unfolded fvars. The behavior in this PR lets metaprograms control whether they want to persist any of the unfolded fvars in this context themselves. In practice, metaprograms that use `withResetZetaDeltaFVarIds` are creating many temporary fvars and are doing dependence computations. These temporary fvars shouldn't be persisted, and also dependence shouldn't be inferred from the fact that a dependence calculation was done. (Concrete example: the let-to-have transformation in an upcoming PR can be run from within simp. Just because let-to-have unfolds an fvar while calculating dependencies of lets doesn't mean that this fvar should be included by `simp?`.)
1 parent d5d0176 commit 267b8ed

File tree

5 files changed

+179
-24
lines changed

5 files changed

+179
-24
lines changed

src/Lean/Elab/MutualDef.lean

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1014,7 +1014,6 @@ def main (sectionVars : Array Expr) (mainHeaders : Array DefViewElabHeader) (mai
10141014
let letRecsToLift := letRecsToLift.toArray
10151015
let mainFVarIds := mainFVars.map Expr.fvarId!
10161016
let recFVarIds := (letRecsToLift.map fun toLift => toLift.fvarId) ++ mainFVarIds
1017-
resetZetaDeltaFVarIds
10181017
withTrackingZetaDelta do
10191018
-- By checking `toLift.type` and `toLift.val` we populate `zetaFVarIds`. See comments at `src/Lean/Meta/Closure.lean`.
10201019
let letRecsToLift ← letRecsToLift.mapM fun toLift => withLCtx toLift.lctx toLift.localInstances do

src/Lean/Meta/Basic.lean

Lines changed: 53 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -687,12 +687,6 @@ def getConfig : MetaM Config :=
687687
def getConfigWithKey : MetaM ConfigWithKey :=
688688
return (← getConfig).toConfigWithKey
689689

690-
def resetZetaDeltaFVarIds : MetaM Unit :=
691-
modify fun s => { s with zetaDeltaFVarIds := {} }
692-
693-
def getZetaDeltaFVarIds : MetaM FVarIdSet :=
694-
return (← get).zetaDeltaFVarIds
695-
696690
/-- Return the array of postponed universe level constraints. -/
697691
def getPostponed : MetaM (PersistentArray PostponedEntry) :=
698692
return (← get).postponed
@@ -1120,15 +1114,12 @@ def elimMVarDeps (xs : Array Expr) (e : Expr) (preserveOrder : Bool := false) :
11201114
modify fun s => { s with cache := cacheSaved }
11211115

11221116
/--
1123-
Executes `x` tracking zetaDelta reductions `Config.trackZetaDelta := true`
1124-
-/
1125-
@[inline] def withTrackingZetaDelta : n α → n α :=
1126-
mapMetaM fun x =>
1127-
withFreshCache <| withReader (fun ctx => { ctx with trackZetaDelta := true }) x
1117+
If `s` is nonempty, then `withZetaDeltaSet s x` executes `x` with `zetaDeltaSet := s`.
1118+
The cache is temporarily reset while executing `x`.
11281119
1129-
/--
1130-
`withZetaDeltaSet s x` executes `x` with `zetaDeltaSet := s`.
1131-
The cache is reset while executing `x` if `s` is not empty.
1120+
If `s` is empty, then runs `x` without changing `zetaDeltaSet` or resetting the cache.
1121+
1122+
See also `withTrackingZetaDeltaSet` for tracking zeta-delta reductions.
11321123
-/
11331124
def withZetaDeltaSet (s : FVarIdSet) : n α → n α :=
11341125
mapMetaM fun x =>
@@ -1138,14 +1129,59 @@ def withZetaDeltaSet (s : FVarIdSet) : n α → n α :=
11381129
withFreshCache <| withReader (fun ctx => { ctx with zetaDeltaSet := s }) x
11391130

11401131
/--
1141-
Similar to `withZetaDeltaSet`, but also enables `withTrackingZetaDelta` if `s` is not empty.
1132+
Gets the current `zetaDeltaFVarIds` set.
1133+
If `Context.trackZetaDelta` is true, then `whnf` adds to this set
1134+
those local definitions that are unfolded ("zeta-delta reduced") .
1135+
1136+
See `withTrackingZetaDelta` and `withTrackingZetaDeltaSet`.
1137+
-/
1138+
def getZetaDeltaFVarIds : MetaM FVarIdSet :=
1139+
return (← get).zetaDeltaFVarIds
1140+
1141+
/--
1142+
`withResetZetaDeltaFVarIds x` executes `x` in a context where the `zetaDeltaFVarIds` is temporarily cleared.
1143+
1144+
**Warning:** This does not reset the cache.
1145+
-/
1146+
@[inline] private def withResetZetaDeltaFVarIds : n α → n α :=
1147+
mapMetaM fun x => do
1148+
let zetaDeltaFVarIdsSaved ← modifyGet fun s => (s.zetaDeltaFVarIds, { s with zetaDeltaFVarIds := {} })
1149+
try
1150+
x
1151+
finally
1152+
modify fun s => { s with zetaDeltaFVarIds := zetaDeltaFVarIdsSaved }
1153+
1154+
/--
1155+
`withTrackingZetaDelta x` executes `x` while tracking zeta-delta reductions performed by `whnf`.
1156+
Furthermore, the `zetaDeltaFVarIds` set is temporarily cleared,
1157+
and also the cache is temporarily reset so that reductions are accurately tracked.
1158+
1159+
Any zeta-delta reductions recorded while executing `x` will *not* persist when leaving `withTrackingZetaDelta`.
1160+
-/
1161+
@[inline] def withTrackingZetaDelta : n α → n α :=
1162+
mapMetaM fun x =>
1163+
withFreshCache <| withReader (fun ctx => { ctx with trackZetaDelta := true }) <| withResetZetaDeltaFVarIds x
1164+
1165+
@[deprecated withTrackingZetaDelta (since := "2025-06-12")]
1166+
def resetZetaDeltaFVarIds : MetaM Unit :=
1167+
modify fun s => { s with zetaDeltaFVarIds := {} }
1168+
1169+
/--
1170+
`withTrackingZetaDeltaSet s x` executes `x` in a context where `zetaDeltaFVarIds` has been temporarily cleared.
1171+
- If `s` is nonempty, zeta-delta tracking is enabled and `zetaDeltaSet := s`.
1172+
Furthermore, the cache is temporarily reset so that zeta-delta tracking is accurate.
1173+
- If `s` is empty, then zeta-delta tracking is disabled. The `zetaDeltaSet` is *not* modified, and the cache is not cleared.
1174+
1175+
Any zeta-delta reductions recorded while executing `x` will *not* persist when leaving `withTrackingZetaDeltaSet`.
1176+
1177+
See also `withZetaDeltaSet`, which does not interact with zeta-delta tracking.
11421178
-/
11431179
def withTrackingZetaDeltaSet (s : FVarIdSet) : n α → n α :=
11441180
mapMetaM fun x =>
11451181
if s.isEmpty then
1146-
x
1182+
withReader (fun ctx => { ctx with trackZetaDelta := false }) <| withResetZetaDeltaFVarIds x
11471183
else
1148-
withFreshCache <| withReader (fun ctx => { ctx with zetaDeltaSet := s, trackZetaDelta := true }) x
1184+
withFreshCache <| withReader (fun ctx => { ctx with zetaDeltaSet := s, trackZetaDelta := true }) <| withResetZetaDeltaFVarIds x
11491185

11501186
@[inline] def withoutProofIrrelevance (x : n α) : n α :=
11511187
withConfig (fun cfg => { cfg with proofIrrelevance := false }) x

src/Lean/Meta/Closure.lean

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,6 @@ structure MkValueTypeClosureResult where
343343
exprArgs : Array Expr
344344

345345
def mkValueTypeClosureAux (type : Expr) (value : Expr) : ClosureM (Expr × Expr) := do
346-
resetZetaDeltaFVarIds
347346
withTrackingZetaDelta do
348347
let type ← collectExpr type
349348
let value ← collectExpr value

src/Lean/Meta/Tactic/Simp/Main.lean

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -800,14 +800,26 @@ where
800800
withTrackingZetaDeltaSet ctx.zetaDeltaSet <|
801801
withReducible x
802802

803-
private def updateUsedSimpsWithZetaDeltaCore (s : UsedSimps) (usedZetaDelta : FVarIdSet) : UsedSimps :=
804-
usedZetaDelta.fold (init := s) fun s fvarId =>
805-
s.insert <| .fvar fvarId
803+
/--
804+
Adds the fvars from `usedZetaDelta` to `s` if they are present in
805+
the set `zetaDeltaSet` of fvars that are explicitly added to the simp context.
806+
807+
*Note:* `usedZetaDelta` might contain fvars that are not in `zetaDeltaSet`,
808+
since within `withResetZetaDeltaFVarIds` it is possible for `whnf` to be run with different configurations,
809+
ones that allow zeta-delta reducing fvars not in `zetaDeltaSet` (e.g. `withInferTypeConfig` sets `zetaDelta := true`).
810+
This also means that `usedZetaDelta` set might be reporting fvars in `zetaDeltaSet` that weren't "used".
811+
-/
812+
private def updateUsedSimpsWithZetaDeltaCore (s : UsedSimps) (zetaDeltaSet : FVarIdSet) (usedZetaDelta : FVarIdSet) : UsedSimps :=
813+
zetaDeltaSet.fold (init := s) fun s fvarId =>
814+
if usedZetaDelta.contains fvarId then
815+
s.insert <| .fvar fvarId
816+
else
817+
s
806818

807819
private def updateUsedSimpsWithZetaDelta (ctx : Context) (stats : Stats) : MetaM Stats := do
808820
let used := stats.usedTheorems
809-
let used := updateUsedSimpsWithZetaDeltaCore used ctx.initUsedZetaDelta
810-
let used := updateUsedSimpsWithZetaDeltaCore used (← getZetaDeltaFVarIds)
821+
let used := updateUsedSimpsWithZetaDeltaCore used ctx.zetaDeltaSet ctx.initUsedZetaDelta
822+
let used := updateUsedSimpsWithZetaDeltaCore used ctx.zetaDeltaSet (← getZetaDeltaFVarIds)
811823
return { stats with usedTheorems := used }
812824

813825
@[inline] def withCatchingRuntimeEx (x : SimpM α) : SimpM α := do

tests/lean/run/6655.lean

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/-!
2+
# Improve zeta-delta tracking for `simp?`
3+
4+
https://github.com/leanprover/lean4/issues/6655 reports issues with `simp?` where
5+
it would over-report local variables. This comes down to two kinds of issues:
6+
- zeta-delta tracking wasn't being reset, so previous `simp?`s would contribute variables
7+
- `simp?` would report variables that weren't explicitly mentioned,
8+
because `whnf` would be run with different configurations during the tracking.
9+
(e.g. `withInferTypeConfig` enables `zetaDelta`.)
10+
11+
This file tests that it resets the tracking and filters the list.
12+
-/
13+
14+
/-!
15+
Example from #6655. This used to suggest `simp only [e, d]`.
16+
-/
17+
/--
18+
info: Try this: simp only [e]
19+
---
20+
trace: α : Type
21+
c : α → α
22+
x : α
23+
d : α → α := c
24+
e : α → α := d
25+
⊢ d x = x
26+
---
27+
warning: declaration uses 'sorry'
28+
-/
29+
#guard_msgs in
30+
example {α : Type} (c : α → α) (x : α) : c x = x := by
31+
let d := c
32+
let e := d
33+
change e x = x
34+
simp? [e]
35+
trace_state
36+
sorry
37+
38+
/-!
39+
Example from #6655. This used to suggest `simp only [d]`.
40+
-/
41+
/--
42+
info: Try this: simp only
43+
---
44+
warning: declaration uses 'sorry'
45+
-/
46+
#guard_msgs in
47+
example {α : Type} (c : α → α) (x : α) : c x = x := by
48+
let d := c
49+
change d x = x
50+
simp [d]
51+
have : x = x := by
52+
simp?
53+
sorry
54+
55+
/-!
56+
Example from comments of #6655. This used to suggest `simp only [Int.add_sub_cancel, p]`.
57+
(N.B. the goal at that point does not have `p` in it!)
58+
-/
59+
/-- info: Try this: simp only [Int.add_sub_cancel] -/
60+
#guard_msgs in
61+
example (a b : Int) : a + b - b = a := by
62+
let p := 1
63+
have h : p = 1 := by
64+
simp only [p]
65+
simp?
66+
67+
/-!
68+
Example from https://github.com/leanprover/lean4/pull/7539 by JovanGerb.
69+
This used to suggest `simp only [a, b] ` and `simp only [a, b]`
70+
-/
71+
/--
72+
info: Try this: simp only [a]
73+
---
74+
info: Try this: simp only
75+
-/
76+
#guard_msgs in
77+
example : True := by
78+
let a := 1
79+
let b := 2
80+
have : b = 2 := by simp [a,b]
81+
have : a = 1 := by simp? [a]
82+
have : 1 = 1 := by simp?
83+
trivial
84+
85+
/-!
86+
Test that there is still a deficiency. This should say `simp only [e]`.
87+
-/
88+
/--
89+
info: Try this: simp only [e, c]
90+
---
91+
trace: α : Type
92+
b : α → α
93+
x : α
94+
c : α → α := b
95+
d : α → α := c
96+
e : α → α := d
97+
⊢ d x = x
98+
---
99+
warning: declaration uses 'sorry'
100+
-/
101+
#guard_msgs in
102+
example {α : Type} (b : α → α) (x : α) : b x = x := by
103+
let c := b
104+
let d := c
105+
let e := d
106+
change e x = x
107+
simp? [e, c]
108+
trace_state
109+
sorry

0 commit comments

Comments
 (0)