lean4-htt/src/Lean/Meta/Tactic/Simp/LoopProtection.lean
Joachim Breitner 29298c9f30
feat: linter.loopingSimpArgs (#8865)
This PR allows `simp` to recognize and warn about simp lemmas that are
likely looping in the current simp set. It does so automatically
whenever simplification fails with the dreaded “max recursion depth”
error fails, but it can be made to do it always with `set_option
linter.loopingSimpArgs true`. This check is not on by default because it
is somewhat costly, and can warn about simp calls that still happen to
work.

This closes #5111. In the end, this implemented much simpler logic than
described there (and tried in the abandoned #8688; see that PR
description for more background information), but it didn’t work as well
as I thought. The current logic is:

“Simplify the RHS of the simp theorem, complain if that fails”.

It is a reasonable policy for a Lean project to say that all simp
invocation should be so that this linter does not complain. Often it is
just a matter of explicitly disabling some simp theorems from the
default simp set, to make it clear and robust that in this call, we do
not want them to trigger. But given that often such simp call happen to
work, it’s too pedantic to impose it on everyone.
2025-06-23 07:36:21 +00:00

86 lines
3.4 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
Copyright (c) 2025 Lean FRO, LLC. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Joachim Breitner
-/
prelude
import Lean.Meta.Tactic.Simp.Types
import Lean.Linter.Basic
namespace Lean.Meta.Simp
register_builtin_option linter.loopingSimpArgs : Bool := {
defValue := false
descr := "\
When enabled, `simp` will check if the theorems passed as simp arguments (`simp [thm1]`) \
are possibly looping in the current simp set.\n\
\n\
More precisely, it tries to simplify the right-hand side of the theorem and complains if \
that fails, which it typically does because of running out of recursion depth.\n\
\n\
This is a relatively expensive check, so it i disabled by default, and only run after \
a `simp` call actually failed with a recursion depth error."
}
@[inline] def withFreshUsedTheorems (x : SimpM α) : SimpM α := do
let saved := (← get).usedTheorems
modify fun s => { s with usedTheorems := {} }
try x finally modify fun s => { s with usedTheorems := saved }
private def ppOrigins (origins : Array Origin) : MetaM MessageData := do
return .andList (← origins.mapM (return m!"`{← ppOrigin ·}`")).toList
def mkLoopWarningMsg (thm : SimpTheorem) : SimpM MessageData := do
let mut msg := m!""
msg := msg ++ m!"Possibly looping simp theorem: `{← ppOrigin thm.origin}`"
let mut others := #[]
for other in (← get).usedTheorems.toArray do
if other != thm.origin then
others := others.push other
unless others.isEmpty do
msg := msg ++ .note m!"Possibly caused by: {← ppOrigins others}"
msg := msg ++ .hint' m!"You can disable a simp theorem from the default simp set by \
passing `- theoremName` to `simp`."
pure msg
def shouldCheckLoops (force : Bool) (ctxt : Simp.Context) : CoreM Bool := do
if ctxt.config.singlePass then return false
if force then return true
return linter.loopingSimpArgs.get (← getOptions)
/--
Main entry point to the loop protection mechanis: Checks if the given theorem is looping in the
current simp set, and logs a warning if it does.
Assumes that `withRef` is set appropriately for the warning.
With `force := off`, only runs when `linter.loopingSimpArgs` is enabled and presents it as a
linter. With `force := on` (typically after `simp` threw an exception) it prints plain warnings.
-/
def checkLoops (force : Bool) (ctxt : Simp.Context) (methods : Methods) (thm : SimpTheorem) : MetaM Unit := do
-- No loop checking when disabled or in single pass mode
unless (← shouldCheckLoops force ctxt) do return
-- Ignore theorems depending on the local context for now
if thm.proof.hasFVar then return
let type ← inferType (← thm.getValue)
forallTelescopeReducing type fun _xs type => do
let rhs := (← whnf type).appArg!
let _ ← SimpM.run ctxt (s := {}) (methods := methods) do
-- We ignore the simp result.
-- A different design is possible where we use the simplified RHS to continue
-- rewriting, but it would be a too severe change to `simp`s semantics
tryCatchRuntimeEx do
let _ ← simp rhs
fun ex => do
trace[Meta.Tactic.simp.loopProtection] "loop protection for {← ppOrigin thm.origin}: got exception{indentD ex.toMessageData}"
if force then
logWarning (← mkLoopWarningMsg thm)
else
Linter.logLint linter.loopingSimpArgs (← getRef) (← mkLoopWarningMsg thm)