Compare commits

...

6 Commits

Author SHA1 Message Date
Leonardo de Moura
fc88436e4f chore: disable 2026-01-17 16:18:46 -08:00
Leonardo de Moura
0940bdad3e feat: improve discharger 2026-01-17 16:18:04 -08:00
Leonardo de Moura
1552324632 feat: ensure cache consistency when wellBehavedMethods := false 2026-01-17 16:04:03 -08:00
Leonardo de Moura
7c36e28664 doc: document 2026-01-17 15:48:11 -08:00
Leonardo de Moura
7fc1a63d93 feat: add Discharger.lean 2026-01-17 15:43:33 -08:00
Leonardo de Moura
b13f771603 chore: debugging support 2026-01-17 15:22:33 -08:00
7 changed files with 171 additions and 10 deletions

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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]