Skip to content

Commit 7abc910

Browse files
authored
feat: optimized simp routine for let telescopes (#8968)
This PR adds the following features to `simp`: - A routine for simplifying `have` telescopes in a way that avoids quadratic complexity arising from locally nameless expression representations, like what #6220 did for `letFun` telescopes. Furthermore, simp converts `letFun`s into `have`s (nondependent lets), and we remove the #6220 routine since we are moving away from `letFun` encodings of nondependent lets. - A `+letToHave` configuration option (enabled by default) that converts lets into haves when possible, when `-zeta` is set. Previously Lean would need to do a full typecheck of the bodies of `let`s, but the `letToHave` procedure can skip checking some subexpressions, and it modifies the `let`s in an entire expression at once rather than one at a time. - A `+zetaHave` configuration option, to turn off zeta reduction of `have`s specifically. The motivation is that dependent `let`s can only be dsimped by let, so zeta reducing just the dependent lets is a reasonable way to make progress. The `+zetaHave` option is also added to the meta configuration. - When `simp` is zeta reducing, it now uses an algorithm that avoids complexity quadratic in the depth of the let telescope. - Additionally, the zeta reduction routines in `simp`, `whnf`, and `isDefEq` now all are consistent with how they apply the `zeta`, `zetaHave`, and `zetaUnused` configurations. The `letToFun` option is addressing a TODO in `getSimpLetCase` ("handle a block of nested let decls in a single pass if this becomes a performance problem"). Performance should be compared to before #8804, which temporarily disabled the #6220 optimizations for `letFun` telescopes. Good kernel performance depends on carefully handling the `have` encoding. Due to the way the kernel instantiates bvars (it does *not* beta reduce when instantiating), we cannot use congruence theorems of the form `(have x := v; f x) = (have x ;= v'; f' x)`, since the bodies of the `have`s will not be syntactically equal, which triggers zeta reduction in the kernel in `is_def_eq`. Instead, we work with `f v = f' v'`, where `f` and `f'` are lambda expressions. There is still zeta reduction, but only when converting between these two forms at the outset of the generated proof.
1 parent 05948f1 commit 7abc910

19 files changed

+1258
-273
lines changed

src/Init/MetaTypes.lean

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ structure Config where
123123
-/
124124
zetaUnused : Bool := true
125125
/--
126-
(Unimplemented)
127126
When `false` (default: `true`), then disables zeta reduction of `have` expressions.
128127
If `zeta` is `false`, then this option has no effect.
129128
Unused `have`s are still removed if `zeta` or `zetaUnused` are true.
@@ -253,6 +252,7 @@ structure Config where
253252
/--
254253
When `true` (default : `true`), then `simp` will remove unused `let` and `have` expressions:
255254
`let x := v; e` simplifies to `e` when `x` does not occur in `e`.
255+
This option takes precedence over `zeta` and `zetaHave`.
256256
-/
257257
zetaUnused : Bool := true
258258
/--
@@ -261,14 +261,12 @@ structure Config where
261261
-/
262262
catchRuntime : Bool := true
263263
/--
264-
(Unimplemented)
265264
When `false` (default: `true`), then disables zeta reduction of `have` expressions.
266265
If `zeta` is `false`, then this option has no effect.
267266
Unused `have`s are still removed if `zeta` or `zetaUnused` are true.
268267
-/
269268
zetaHave : Bool := true
270269
/--
271-
(Unimplemented)
272270
When `true` (default : `true`), then `simp` will attempt to transform `let`s into `have`s
273271
if they are non-dependent. This only applies when `zeta := false`.
274272
-/

src/Init/SimpLemmas.lean

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,22 @@ theorem let_body_congr {α : Sort u} {β : α → Sort v} {b b' : (a : α) →
7474
(a : α) (h : ∀ x, b x = b' x) : (let x := a; b x) = (let x := a; b' x) :=
7575
(funext h : b = b') ▸ rfl
7676

77+
/-!
78+
Congruence lemmas for `have` have kernel performance issues when stated using `have` directly.
79+
Illustration of the problem: the kernel infers that the type of
80+
`have_congr (fun x => b) (fun x => b') h₁ h₂`
81+
is
82+
`(have x := a; (fun x => b) x) = (have x := a'; (fun x => b') x)`
83+
rather than
84+
`(have x := a; b x) = (have x := a'; b' x)`
85+
That means the kernel will do `whnf_core` at every step of checking a sequence of these lemmas.
86+
Thus, we get quadratically many zeta reductions.
87+
88+
For reference, we have the `have` versions of the theorems in the following comment,
89+
and then after that we have the versions that `simpHaveTelescope` actually uses,
90+
which avoid this issue.
91+
-/
92+
/-
7793
theorem have_unused {α : Sort u} {β : Sort v} (a : α) {b b' : β}
7894
(h : b = b') : (have _ := a; b) = b' := h
7995
@@ -95,6 +111,29 @@ theorem have_body_congr_dep {α : Sort u} {β : α → Sort v} (a : α) {f f' :
95111
theorem have_body_congr {α : Sort u} {β : Sort v} (a : α) {f f' : α → β}
96112
(h : ∀ x, f x = f' x) : (have x := a; f x) = (have x := a; f' x) :=
97113
h a
114+
-/
115+
116+
theorem have_unused' {α : Sort u} {β : Sort v} (a : α) {b b' : β}
117+
(h : b = b') : (fun _ => b) a = b' := h
118+
119+
theorem have_unused_dep' {α : Sort u} {β : Sort v} (a : α) {b : α → β} {b' : β}
120+
(h : ∀ x, b x = b') : b a = b' := h a
121+
122+
theorem have_congr' {α : Sort u} {β : Sort v} {a a' : α} {f f' : α → β}
123+
(h₁ : a = a') (h₂ : ∀ x, f x = f' x) : f a = f' a' :=
124+
@congr α β f f' a a' (funext h₂) h₁
125+
126+
theorem have_val_congr' {α : Sort u} {β : Sort v} {a a' : α} {f : α → β}
127+
(h : a = a') : f a = f a' :=
128+
@congrArg α β a a' f h
129+
130+
theorem have_body_congr_dep' {α : Sort u} {β : α → Sort v} (a : α) {f f' : (x : α) → β x}
131+
(h : ∀ x, f x = f' x) : f a = f' a :=
132+
h a
133+
134+
theorem have_body_congr' {α : Sort u} {β : Sort v} (a : α) {f f' : α → β}
135+
(h : ∀ x, f x = f' x) : f a = f' a :=
136+
h a
98137

99138
theorem letFun_unused {α : Sort u} {β : Sort v} (a : α) {b b' : β} (h : b = b') : @letFun α (fun _ => β) a (fun _ => b) = b' :=
100139
h

src/Lean/Elab/PreDefinition/WF/Preprocess.lean

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ private def nonPropHaveToLet (e : Expr) : MetaM Expr := do
176176
def preprocess (e : Expr) : MetaM Simp.Result := do
177177
unless wf.preprocess.get (← getOptions) do
178178
return { expr := e }
179+
-- Transform `let`s to `have`s to enable `simp` entering let bodies.
180+
let e ← letToHave e
179181
lambdaTelescope e fun xs _ => do
180182
-- Annotate all xs with `wfParam`
181183
let xs' ← xs.mapM mkWfParam

src/Lean/Meta/Basic.lean

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,8 @@ structure Config where
158158
/-- Control projection reduction at `whnfCore`. -/
159159
proj : ProjReductionKind := .yesWithDelta
160160
/--
161-
Zeta reduction: `let x := v; e[x]` reduces to `e[v]`.
162-
We say a let-declaration `let x := v; e` is non dependent if it is equivalent to `(fun x => e) v`.
161+
Zeta reduction: `let x := v; e[x]` and `have x := v; e[x]` reduce to `e[v]`.
162+
We say a let-declaration `let x := v; e` is nondependent if it is equivalent to `(fun x => e) v`.
163163
Recall that
164164
```
165165
fun x : BitVec 5 => let n := 5; fun y : BitVec n => x = y
@@ -169,6 +169,7 @@ structure Config where
169169
fun x : BitVec 5 => (fun n => fun y : BitVec n => x = y) 5
170170
```
171171
is not.
172+
See also `zetaHave`, for disabling the reduction nondependent lets (`have` expressions).
172173
-/
173174
zeta : Bool := true
174175
/--
@@ -178,8 +179,13 @@ structure Config where
178179
/--
179180
Zeta reduction for unused let-declarations: `let x := v; e` reduces to `e` when `x` does not occur
180181
in `e`.
182+
This option takes precedence over `zeta` and `zetaHave`.
181183
-/
182184
zetaUnused : Bool := true
185+
/--
186+
When `zeta := true`, then `zetaHave := false` disables zeta reduction of `have` expressions.
187+
-/
188+
zetaHave : Bool := true
183189
deriving Inhabited, Repr
184190

185191
/-- Convert `isDefEq` and `WHNF` relevant parts into a key for caching results -/
@@ -200,7 +206,8 @@ private def Config.toKey (c : Config) : UInt64 :=
200206
(c.zetaDelta.toUInt64 <<< 14) |||
201207
(c.univApprox.toUInt64 <<< 15) |||
202208
(c.etaStruct.toUInt64 <<< 16) |||
203-
(c.proj.toUInt64 <<< 18)
209+
(c.proj.toUInt64 <<< 18) |||
210+
(c.zetaHave.toUInt64 <<< 20)
204211

205212
/-- Configuration with key produced by `Config.toKey`. -/
206213
structure ConfigWithKey where

src/Lean/Meta/ExprDefEq.lean

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,20 +1641,14 @@ private def isDefEqMVarSelf (mvar : Expr) (args₁ args₂ : Array Expr) : MetaM
16411641
pure false
16421642

16431643
/--
1644-
Removes unnecessary let-decls (both true `let`s and `let_fun`s).
1644+
Consumes unused lets/haves, depending on the current configuration.
1645+
- When `zetaUnused`, all unused lets may be consumed.
1646+
- Otherwise, when `zeta` is true, then unused lets can be consumed, unless they are nondependent and `cfg.zetaHave` is false.
16451647
-/
1646-
private partial def consumeLet : Expr → Expr
1647-
| e@(.letE _ _ _ b _) => if b.hasLooseBVars then e else consumeLet b
1648-
| e =>
1649-
if let some (_, _, _, b) := e.letFun? then
1650-
if b.hasLooseBVars then e else consumeLet b
1651-
else
1652-
e
1653-
16541648
private partial def consumeLetIfZeta (e : Expr) : MetaM Expr := do
16551649
let cfg ← getConfig
16561650
if cfg.zeta || cfg.zetaUnused then
1657-
return consumeLet e
1651+
return consumeUnusedLet e (consumeNondep := cfg.zetaUnused || cfg.zetaHave)
16581652
else
16591653
return e
16601654

src/Lean/Meta/InferType.lean

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,10 @@ because it overrides unrelated configurations.
189189
@[inline] def withInferTypeConfig (x : MetaM α) : MetaM α :=
190190
withAtLeastTransparency .default do
191191
let cfg ← getConfig
192-
if cfg.beta && cfg.iota && cfg.zeta && cfg.zetaDelta && cfg.proj == .yesWithDelta then
192+
if cfg.beta && cfg.iota && cfg.zeta && cfg.zetaHave && cfg.zetaDelta && cfg.proj == .yesWithDelta then
193193
x
194194
else
195-
withConfig (fun cfg => { cfg with beta := true, iota := true, zeta := true, zetaDelta := true, proj := .yesWithDelta }) x
195+
withConfig (fun cfg => { cfg with beta := true, iota := true, zeta := true, zetaHave := true, zetaDelta := true, proj := .yesWithDelta }) x
196196

197197
@[export lean_infer_type]
198198
def inferTypeImp (e : Expr) : MetaM Expr :=

0 commit comments

Comments
 (0)