feat: Sym.simp dischargers (#12032)
This PR adds `Discharger`s to `Sym.simp`, and ensures the cached results are consistent.
This commit is contained in:
parent
6a87c0e530
commit
de6ff061ed
7 changed files with 171 additions and 10 deletions
|
|
@ -19,3 +19,4 @@ public import Lean.Meta.Sym.Simp.Lambda
|
|||
public import Lean.Meta.Sym.Simp.Forall
|
||||
public import Lean.Meta.Sym.Simp.Debug
|
||||
public import Lean.Meta.Sym.Simp.EvalGround
|
||||
public import Lean.Meta.Sym.Simp.Discharger
|
||||
|
|
|
|||
|
|
@ -17,11 +17,14 @@ open Simp
|
|||
Helper functions for debugging purposes and creating tests.
|
||||
-/
|
||||
|
||||
public def mkMethods (declNames : Array Name) : MetaM Methods := do
|
||||
public def mkSimprocFor (declNames : Array Name) : MetaM Simproc := do
|
||||
let mut thms : Theorems := {}
|
||||
for declName in declNames do
|
||||
thms := thms.insert (← mkTheoremFromDecl declName)
|
||||
return { post := thms.rewrite }
|
||||
return thms.rewrite
|
||||
|
||||
public def mkMethods (declNames : Array Name) : MetaM Methods := do
|
||||
return { post := (← mkSimprocFor declNames) }
|
||||
|
||||
public def simpWith (k : Expr → SymM Result) (mvarId : MVarId) : MetaM (Option MVarId) := SymM.run do
|
||||
let mvarId ← preprocessMVar mvarId
|
||||
|
|
|
|||
121
src/Lean/Meta/Sym/Simp/Discharger.lean
Normal file
121
src/Lean/Meta/Sym/Simp/Discharger.lean
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
/-
|
||||
Copyright (c) 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
Released under Apache 2.0 license as described in the file LICENSE.
|
||||
Authors: Leonardo de Moura
|
||||
-/
|
||||
module
|
||||
prelude
|
||||
public import Lean.Meta.Sym.Simp.SimpM
|
||||
import Lean.Meta.AppBuilder
|
||||
namespace Lean.Meta.Sym.Simp
|
||||
|
||||
/-!
|
||||
# Dischargers for Conditional Rewrite Rules
|
||||
|
||||
This module provides dischargers for handling conditional rewrite rules in `Sym.simp`.
|
||||
A discharger attempts to prove side conditions that arise during rewriting.
|
||||
|
||||
## Overview
|
||||
|
||||
When applying a conditional rewrite rule `h : P → a = b`, the simplifier must prove
|
||||
the precondition `P` before using the rule. A `Discharger` is a function that attempts
|
||||
to construct such proofs.
|
||||
|
||||
**Example**: Consider the rewrite rule:
|
||||
```
|
||||
theorem div_self (n : Nat) (h : n ≠ 0) : n / n = 1
|
||||
```
|
||||
When simplifying `x / x`, the discharger must prove `x ≠ 0` to apply this rule.
|
||||
|
||||
## Design
|
||||
|
||||
Dischargers work by:
|
||||
1. Attempting to simplify the side condition to `True`
|
||||
2. If successful, extracting a proof from the simplification result
|
||||
3. Returning `none` if the condition cannot be discharged
|
||||
|
||||
This integrates naturally with `Simproc`-based simplification.
|
||||
|
||||
## Important
|
||||
|
||||
When using dischargers that access new local declarations introduced when
|
||||
visiting binders, it is the user's responsibility to set `wellBehavedMethods := false`.
|
||||
This setting will instruct `simp` to discard the cache after visiting the binder's body.
|
||||
-/
|
||||
|
||||
/--
|
||||
A discharger attempts to prove propositions that arise as side conditions during rewriting.
|
||||
|
||||
Given a proposition `e : Prop`, returns:
|
||||
- `some proof` if `e` can be proven
|
||||
- `none` if `e` cannot be discharged
|
||||
|
||||
**Usage**: Dischargers are used by the simplifier when applying conditional rewrite rules.
|
||||
-/
|
||||
public abbrev Discharger := Expr → SimpM (Option Expr)
|
||||
|
||||
def resultToOptionProof (e : Expr) (result : Result) : Option Expr :=
|
||||
match result with
|
||||
| .rfl _ => none
|
||||
| .step e' h _ =>
|
||||
if e'.isTrue then
|
||||
some <| mkOfEqTrueCore e h
|
||||
else
|
||||
none
|
||||
|
||||
/--
|
||||
Converts a simplification procedure into a discharger.
|
||||
|
||||
A `Simproc` can be used as a discharger by simplifying the side condition and
|
||||
checking if it reduces to `True`. If so, the equality proof is converted to
|
||||
a proof of the original proposition.
|
||||
|
||||
**Algorithm**:
|
||||
1. Apply the simproc to the side condition `e`
|
||||
2. If `e` simplifies to `True` (via proof `h : e = True`), return `ofEqTrue h : e`
|
||||
3. Otherwise, return `none` (cannot discharge)
|
||||
|
||||
**Parameters**:
|
||||
- `p`: A simplification procedure to use for discharging conditions
|
||||
|
||||
**Example**: If `p` simplifies `5 < 10` to `True` via proof `h : (5 < 10) = True`,
|
||||
then `mkDischargerFromSimproc p` returns `ofEqTrue h : 5 < 10`.
|
||||
-/
|
||||
public def mkDischargerFromSimproc (p : Simproc) : Discharger := fun e => do
|
||||
return resultToOptionProof e (← p e)
|
||||
|
||||
/--
|
||||
The default discharger uses the simplifier itself to discharge side conditions.
|
||||
|
||||
This creates a natural recursive behavior: when applying conditional rules,
|
||||
the simplifier is invoked to prove their preconditions. This is effective because:
|
||||
|
||||
1. **Ground terms**: Conditions like `5 ≠ 0` are evaluated by simprocs
|
||||
2. **Recursive simplification**: Complex conditions are reduced to simpler ones
|
||||
3. **Lemma application**: The simplifier can apply other rewrite rules to conditions
|
||||
|
||||
It ensures the cached results are discarded, and increases the discharge depth to avoid
|
||||
infinite recursion.
|
||||
-/
|
||||
public def dischargeSimpSelf : Discharger := fun e => do
|
||||
if (← readThe Context).dischargeDepth > (← getConfig).maxDischargeDepth then
|
||||
return none
|
||||
withoutModifyingCache do
|
||||
withTheReader Context (fun ctx => { ctx with dischargeDepth := ctx.dischargeDepth + 1 }) do
|
||||
return resultToOptionProof e (← simp e)
|
||||
|
||||
/--
|
||||
A discharger that fails to prove any side condition.
|
||||
|
||||
This is used when conditional rewrite rules should not be applied. It immediately
|
||||
returns `none` for all propositions, effectively disabling conditional rewriting.
|
||||
|
||||
**Use cases**:
|
||||
- Testing: Isolating unconditional rewriting behavior
|
||||
- Performance: Avoiding expensive discharge attempts when conditions are unlikely to hold
|
||||
- Controlled rewriting: Explicitly disabling conditional rules in specific contexts
|
||||
-/
|
||||
public def dischargeNone : Discharger := fun _ =>
|
||||
return none
|
||||
|
||||
end Lean.Meta.Sym.Simp
|
||||
|
|
@ -80,7 +80,12 @@ public def simpForall (e : Expr) : SimpM Result := do
|
|||
simpArrow e
|
||||
else if (← isProp e) then
|
||||
let n := getForallTelescopeSize e.bindingBody! 1
|
||||
forallBoundedTelescope e n fun xs b => do
|
||||
forallBoundedTelescope e n fun xs b => withoutModifyingCacheIfNotWellBehaved do
|
||||
main xs b
|
||||
else
|
||||
return .rfl
|
||||
where
|
||||
main (xs : Array Expr) (b : Expr) : SimpM Result := do
|
||||
match (← simp b) with
|
||||
| .rfl _ => return .rfl
|
||||
| .step b' h _ =>
|
||||
|
|
@ -89,9 +94,7 @@ public def simpForall (e : Expr) : SimpM Result := do
|
|||
-- **Note**: consider caching the forall-congr theorems
|
||||
let hcongr ← mkForallCongrFor xs
|
||||
return .step e' (mkApp3 hcongr (← mkLambdaFVars xs b) (← mkLambdaFVars xs b') h)
|
||||
else
|
||||
return .rfl
|
||||
where
|
||||
|
||||
-- **Note**: Optimize if this is quadratic in practice
|
||||
getForallTelescopeSize (e : Expr) (n : Nat) : Nat :=
|
||||
match e with
|
||||
|
|
|
|||
|
|
@ -47,7 +47,10 @@ def mkFunextFor (xs : Array Expr) (β : Expr) : MetaM Expr := do
|
|||
return result
|
||||
|
||||
public def simpLambda (e : Expr) : SimpM Result := do
|
||||
lambdaTelescope e fun xs b => do
|
||||
lambdaTelescope e fun xs b => withoutModifyingCacheIfNotWellBehaved do
|
||||
main xs b
|
||||
where
|
||||
main (xs : Array Expr) (b : Expr) : SimpM Result := do
|
||||
match (← simp b) with
|
||||
| .rfl _ => return .rfl
|
||||
| .step b' h _ =>
|
||||
|
|
@ -55,7 +58,7 @@ public def simpLambda (e : Expr) : SimpM Result := do
|
|||
let e' ← shareCommonInc (← mkLambdaFVars xs b')
|
||||
let funext ← getFunext xs b
|
||||
return .step e' (mkApp3 funext e e' h)
|
||||
where
|
||||
|
||||
getFunext (xs : Array Expr) (b : Expr) : SimpM Expr := do
|
||||
let key ← inferType e
|
||||
if let some h := (← get).funext.find? { expr := key } then
|
||||
|
|
|
|||
|
|
@ -102,6 +102,11 @@ invalidating the cache and causing O(2^n) behavior on conditional trees.
|
|||
structure Config where
|
||||
/-- Maximum number of steps that can be performed by the simplifier. -/
|
||||
maxSteps : Nat := 1000
|
||||
/--
|
||||
Maximum depth of reentrant simplifier calls through dischargers.
|
||||
Prevents infinite loops when conditional rewrite rules trigger recursive discharge attempts.
|
||||
-/
|
||||
maxDischargeDepth : Nat := 2
|
||||
|
||||
/--
|
||||
The result of simplifying an expression `e`.
|
||||
|
|
@ -161,6 +166,7 @@ structure Context where
|
|||
/-- Size of the local context when simplification started.
|
||||
Used to determine which free variables were introduced during simplification. -/
|
||||
initialLCtxSize : Nat
|
||||
dischargeDepth : Nat := 0
|
||||
|
||||
/-- Cache mapping expressions (by pointer equality) to their simplified results. -/
|
||||
abbrev Cache := PHashMap ExprPtr Result
|
||||
|
|
@ -193,11 +199,11 @@ structure Methods where
|
|||
post : Simproc := fun _ => return .rfl
|
||||
/--
|
||||
`wellBehavedMethods` must **not** be set to `true` IF their behavior
|
||||
depends on hypotheses in the local context. For example, for applying
|
||||
depends on new hypotheses in the local context. For example, for applying
|
||||
conditional rewrite rules.
|
||||
Reason: it would prevent us from aggressively caching `simp` results.
|
||||
-/
|
||||
wellBehavedDischarge : Bool := true
|
||||
wellBehavedMethods : Bool := true
|
||||
deriving Inhabited
|
||||
|
||||
unsafe def Methods.toMethodsRefImpl (m : Methods) : MethodsRef :=
|
||||
|
|
@ -235,6 +241,13 @@ abbrev pre : Simproc := fun e => do
|
|||
abbrev post : Simproc := fun e => do
|
||||
(← getMethods).post e
|
||||
|
||||
abbrev withoutModifyingCache (k : SimpM α) : SimpM α := do
|
||||
let cache ← getCache
|
||||
try k finally modify fun s => { s with cache }
|
||||
|
||||
abbrev withoutModifyingCacheIfNotWellBehaved (k : SimpM α) : SimpM α := do
|
||||
if (← getMethods).wellBehavedMethods then k else withoutModifyingCache k
|
||||
|
||||
end Simp
|
||||
|
||||
abbrev simp (e : Expr) (methods : Simp.Methods := {}) (config : Simp.Config := {}) : SymM Simp.Result := do
|
||||
|
|
|
|||
17
tests/lean/run/sym_simp_3.lean
Normal file
17
tests/lean/run/sym_simp_3.lean
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import Lean
|
||||
open Lean Meta Elab Tactic
|
||||
|
||||
elab "sym_simp" "[" declNames:ident,* "]" : tactic => do
|
||||
let rewrite ← Sym.mkSimprocFor (← declNames.getElems.mapM fun s => realizeGlobalConstNoOverload s.raw)
|
||||
let methods : Sym.Simp.Methods := { post := Sym.Simp.evalGround.andThen rewrite }
|
||||
liftMetaTactic1 <| Sym.simpWith (Sym.simp · methods)
|
||||
|
||||
example : (1-1) + x*1 + (2-1)*0 = x := by
|
||||
sym_simp [Nat.add_zero, Nat.zero_add, Nat.mul_one]
|
||||
|
||||
opaque f : Nat → Nat
|
||||
axiom fax : x > 10 → f x = 0
|
||||
|
||||
-- TODO:
|
||||
-- example : f 12 = 0 := by
|
||||
-- sym_simp [fax]
|
||||
Loading…
Add table
Reference in a new issue