This PR implements first-class support for nondependent let expressions in the elaborator; recall that a let expression `let x : t := v; b` is called *nondependent* if `fun x : t => b` typechecks, and the notation for a nondependent let expression is `have x := v; b`. Previously we encoded `have` using the `letFun` function, but now we make use of the `nondep` flag in the `Expr.letE` constructor for the encoding. This has been given full support throughout the metaprogramming interface and the elaborator. Key changes to the metaprogramming interface: - Local context `ldecl`s with `nondep := true` are generally treated as `cdecl`s. This is because in the body of a `have` expression the variable is opaque. Functions like `LocalDecl.isLet` by default return `false` for nondependent `ldecl`s. In the rare case where it is needed, they take an additional optional `allowNondep : Bool` flag (defaults to `false`) if the variable is being processed in a context where the value is relevant. - Functions such as `mkLetFVars` by default generalize nondependent let variables and create lambda expressions for them. The `generalizeNondepLet` flag (default true) can be set to false if `have` expressions should be produced instead. **Breaking change:** Uses of `letLambdaTelescope`/`mkLetFVars` need to use `generalizeNondepLet := false`. See the next item. - There are now some mapping functions to make telescoping operations more convenient. See `mapLetTelescope` and `mapLambdaLetTelescope`. There is also `mapLetDecl` as a counterpart to `withLetDecl` for creating `let`/`have` expressions. - Important note about the `generalizeNondepLet` flag: it should only be used for variables in a local context that the metaprogram "owns". Since nondependent let variables are treated as constants in most cases, the `value` field might refer to variables that do not exist, if for example those variables were cleared or reverted. Using `mapLetDecl` is always fine. - The simplifier will cache its let dependence calculations in the nondep field of let expressions. - The `intro` tactic still produces *dependent* local variables. Given that the simplifier will transform lets into haves, it would be surprising if that would prevent `intro` from creating a local variable whose value cannot be used. Note that nondependence of lets is not checked by the kernel. To external checker authors: If the elaborator gets the nondep flag wrong, we consider this to be an elaborator error. Feel free to typecheck `letE n t v b true` as if it were `app (lam n t b default) v` and please report issues. This PR follows up from #8751, which made sure the nondep flag was preserved in the C++ interface.
91 lines
3.8 KiB
Text
91 lines
3.8 KiB
Text
/-
|
|
Copyright (c) 2020 Microsoft Corporation. All rights reserved.
|
|
Released under Apache 2.0 license as described in the file LICENSE.
|
|
Authors: Leonardo de Moura
|
|
-/
|
|
prelude
|
|
import Init.Grind.Util
|
|
import Lean.Meta.Closure
|
|
import Lean.Meta.Transform
|
|
|
|
namespace Lean.Meta
|
|
|
|
/-- Abstracts the given proof into an auxiliary theorem, suitably pre-processing its type. -/
|
|
def abstractProof [Monad m] [MonadLiftT MetaM m] [MonadEnv m] [MonadOptions m] [MonadFinally m]
|
|
(proof : Expr) (cache := true) (postprocessType : Expr → m Expr := pure) : m Expr := do
|
|
let type ← withoutExporting do inferType proof
|
|
let type ← (Core.betaReduce type : MetaM _)
|
|
let type ← zetaReduce type
|
|
let type ← postprocessType type
|
|
/- We turn on zetaDelta-expansion to make sure we don't need to perform an expensive `check` step to
|
|
identify which let-decls can be abstracted. If we design a more efficient test, we can avoid the eager zetaDelta expansion step.
|
|
In a benchmark created by @selsam, The extra `check` step was a bottleneck. -/
|
|
mkAuxTheorem (cache := cache) type proof (zetaDelta := true)
|
|
|
|
namespace AbstractNestedProofs
|
|
|
|
def getLambdaBody (e : Expr) : Expr :=
|
|
match e with
|
|
| .lam _ _ b _ => getLambdaBody b
|
|
| _ => e
|
|
|
|
def isNonTrivialProof (e : Expr) : MetaM Bool := do
|
|
if !(← isProof e) then
|
|
return false
|
|
else if e.isAppOf ``Grind.nestedProof then
|
|
-- Grind.nestedProof is a gadget created by the `grind` tactic.
|
|
-- We want to avoid the situation where `grind` keeps creating them,
|
|
-- and this module, which is used by `grind`, keeps abstracting them.
|
|
return false
|
|
else
|
|
-- We consider proofs such as `fun x => f x a` as trivial.
|
|
-- For example, we don't want to abstract the body of `def rfl`
|
|
(getLambdaBody e).withApp fun f args =>
|
|
pure $ !f.isAtomic || args.any fun arg => !arg.isAtomic
|
|
|
|
structure Context where
|
|
cache : Bool
|
|
|
|
abbrev M := ReaderT Context $ MonadCacheT ExprStructEq Expr MetaM
|
|
|
|
partial def visit (e : Expr) : M Expr := do
|
|
if e.isAtomic then
|
|
pure e
|
|
else
|
|
let visitBinders (xs : Array Expr) (k : M Expr) : M Expr := do
|
|
let localInstances ← getLocalInstances
|
|
let mut lctx ← getLCtx
|
|
for x in xs do
|
|
let xFVarId := x.fvarId!
|
|
let localDecl ← xFVarId.getDecl
|
|
let type ← visit localDecl.type
|
|
let localDecl := localDecl.setType type
|
|
let localDecl ← match localDecl.value? (allowNondep := true) with
|
|
| some value => let value ← visit value; pure <| localDecl.setValue value
|
|
| none => pure localDecl
|
|
lctx := lctx.modifyLocalDecl xFVarId fun _ => localDecl
|
|
withLCtx lctx localInstances k
|
|
checkCache { val := e : ExprStructEq } fun _ => do
|
|
if (← withoutExporting do isNonTrivialProof e) then
|
|
/- Ensure proofs nested in type are also abstracted -/
|
|
abstractProof e (← read).cache visit
|
|
else match e with
|
|
| .lam ..
|
|
| .letE .. => lambdaLetTelescope e fun xs b => visitBinders xs do mkLambdaFVars xs (← visit b) (usedLetOnly := false) (generalizeNondepLet := false)
|
|
| .forallE .. => forallTelescope e fun xs b => visitBinders xs do mkForallFVars xs (← visit b)
|
|
| .mdata _ b => return e.updateMData! (← visit b)
|
|
| .proj _ _ b => return e.updateProj! (← visit b)
|
|
| .app .. => e.withApp fun f args => return mkAppN (← visit f) (← args.mapM visit)
|
|
| _ => pure e
|
|
|
|
end AbstractNestedProofs
|
|
|
|
/-- Replace proofs nested in `e` with new lemmas. The new lemmas are named using `getDeclNGen`. -/
|
|
def abstractNestedProofs (e : Expr) (cache := true) : MetaM Expr := do
|
|
if (← isProof e) then
|
|
-- `e` is a proof itself. So, we don't abstract nested proofs
|
|
return e
|
|
else
|
|
AbstractNestedProofs.visit e |>.run { cache } |>.run
|
|
|
|
end Lean.Meta
|