-
Notifications
You must be signed in to change notification settings - Fork 715
feat: linter.simp.loopProtection #8688
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
f320905 to
1816815
Compare
|
Does this need to be a flag? If it will definitely loop, then no simp call in the wild could be doing it because it would cause timeouts. |
|
This is still rather early WIP and I’m still in the process of figuring out if this should be a flag, or an option, or both. Note that the check may be expensive (TBD), and that the check can have false positives if for the concrete values the lemma is instantiated with the RHS can be rewritten some other, terminating way. So an opt-out is likely needed. |
|
Mathlib CI status (docs):
|
|
!bench |
|
Here are the benchmark results for commit 94269c5. |
e85650a to
c97d911
Compare
|
!bench |
|
Here are the benchmark results for commit 889fba0. Benchmark Metric Change
==================================================================================
- Init.Data.BitVec.Lemmas branches 5.9% (204.2 σ)
- Init.Data.BitVec.Lemmas instructions 5.5% (169.1 σ)
- Init.Data.BitVec.Lemmas re-elab branches 5.5% (321.5 σ)
- Init.Data.BitVec.Lemmas re-elab instructions 5.1% (285.3 σ)
- Init.Data.List.Sublist async branches 4.5% (111.6 σ)
- Init.Data.List.Sublist async instructions 4.4% (97.7 σ)
- Init.Data.List.Sublist re-elab -j4 branch-misses 2.8% (47.1 σ)
- Init.Data.List.Sublist re-elab -j4 branches 3.9% (65.1 σ)
- Init.Data.List.Sublist re-elab -j4 instructions 3.9% (77.7 σ)
- Std.Data.DHashMap.Internal.RawLemmas branch-misses 8.0% (45.6 σ)
- Std.Data.DHashMap.Internal.RawLemmas branches 9.3% (712.3 σ)
- Std.Data.DHashMap.Internal.RawLemmas instructions 9.5% (647.7 σ)
- Std.Data.Internal.List.Associative branches 8.9% (1485.9 σ)
- Std.Data.Internal.List.Associative instructions 8.9% (1226.1 σ)
+ bv_decide_realworld maxrss -4.0% (-30.5 σ)
+ stdlib attribute application -1.2% (-46.9 σ)
- stdlib instructions 1.9% (292.5 σ)
+ stdlib process pre-definitions -2.3% (-42.2 σ) |
0e3ae4d to
1f076b1
Compare
…mp-loop-detection
|
!bench |
|
Here are the benchmark results for commit 34659c4. Benchmark Metric Change
================================================================================
- Init.Data.BitVec.Lemmas branch-misses 2.3% (37.4 σ)
- Init.Data.BitVec.Lemmas branches 3.5% (256.8 σ)
- Init.Data.BitVec.Lemmas instructions 3.3% (219.0 σ)
- Init.Data.BitVec.Lemmas re-elab branches 3.3% (62.1 σ)
- Init.Data.BitVec.Lemmas re-elab instructions 3.1% (60.5 σ)
- Init.Data.BitVec.Lemmas re-elab task-clock 2.5% (36.7 σ)
- Init.Data.List.Sublist async branches 2.6% (67.5 σ)
- Init.Data.List.Sublist async instructions 2.5% (58.4 σ)
- Init.Data.List.Sublist re-elab -j4 branches 2.3% (42.6 σ)
- Init.Data.List.Sublist re-elab -j4 instructions 2.2% (47.6 σ)
- Std.Data.DHashMap.Internal.RawLemmas branches 12.1% (1452.5 σ)
- Std.Data.DHashMap.Internal.RawLemmas instructions 12.3% (1228.7 σ)
- Std.Data.Internal.List.Associative branch-misses 8.8% (32.7 σ)
- Std.Data.Internal.List.Associative branches 10.2% (531.1 σ)
- Std.Data.Internal.List.Associative instructions 10.2% (465.2 σ)
- lake config elab instructions 2.7% (205.3 σ)
- lake config import instructions 5.1% (175.1 σ)
- lake config tree instructions 4.8% (168.4 σ)
- lake env instructions 5.1% (190.9 σ)
- stdlib blocked (unaccounted) 11.6% (28.9 σ)
- stdlib instructions 3.1% (2285.9 σ)
- stdlib tactic execution 63.5% (37.7 σ)
- stdlib task-clock 4.7% (35.3 σ)
- stdlib wall-clock 2.1% (23.4 σ) |
|
Yuck, I was hoping by checking just the simp arguments these numbers would be much better :-( |
|
!bench |
|
Here are the benchmark results for commit 13755a0. Benchmark Metric Change
===================================================================================
- Init.Data.BitVec.Lemmas branches 3.5% (165.0 σ)
- Init.Data.BitVec.Lemmas instructions 3.3% (144.5 σ)
- Init.Data.BitVec.Lemmas re-elab branches 3.3% (60.0 σ)
- Init.Data.BitVec.Lemmas re-elab instructions 3.1% (60.7 σ)
- Init.Data.List.Sublist async branches 2.6% (85.2 σ)
- Init.Data.List.Sublist async instructions 2.5% (75.2 σ)
- Init.Data.List.Sublist re-elab -j4 branches 2.2% (90.0 σ)
- Init.Data.List.Sublist re-elab -j4 instructions 2.2% (95.5 σ)
- Init.Data.List.Sublist re-elab -j4 wall-clock 2.5% (121.4 σ)
- Std.Data.DHashMap.Internal.RawLemmas branch-misses 9.4% (39.7 σ)
- Std.Data.DHashMap.Internal.RawLemmas branches 12.1% (741.1 σ)
- Std.Data.DHashMap.Internal.RawLemmas instructions 12.3% (651.7 σ)
- Std.Data.Internal.List.Associative branch-misses 8.7% (29.9 σ)
- Std.Data.Internal.List.Associative branches 10.2% (751.5 σ)
- Std.Data.Internal.List.Associative instructions 10.2% (670.6 σ)
- bv_decide_large_aig.lean task-clock 7.9% (38.5 σ)
- bv_decide_large_aig.lean wall-clock 7.7% (42.3 σ)
- bv_decide_mod task-clock 7.6% (536.1 σ)
- bv_decide_mod wall-clock 7.6% (93.6 σ)
- lake config elab instructions 2.7% (209.7 σ)
- lake config import instructions 5.1% (126.7 σ)
- lake config tree instructions 4.8% (156.1 σ)
- lake env instructions 5.1% (197.6 σ)
- qsort task-clock 13.7% (25.0 σ)
- qsort wall-clock 13.7% (24.8 σ)
- stdlib blocked 8.8% (488.3 σ)
+ stdlib grind -2.6% (-143.2 σ)
- stdlib instructions 3.1% (11334.2 σ)
- stdlib process pre-definitions 2.3% (85.5 σ)
- stdlib tactic execution 58.3% (33.3 σ)
- stdlib task-clock 4.3% (28.4 σ)
- tests/bench/ interpreted task-clock 5.8% (47.4 σ)
+ workspaceSymbols task-clock -9.0% (-91.5 σ)
+ workspaceSymbols wall-clock -9.1% (-89.6 σ) |
|
I’ll pursue #8865 instead. |
This PR refactors the way simp arguments are elaborated: Instead of changing the `SimpTheorems` structure as we go, this elaborates each argument to a more declarative description of what it does, and then apply those. This enables more interesting checks of simp arguments that need to happen in the context of the eventually constructed simp context (the checks in #8688), or after simp has run (unused argument linter #8901). The new data structure describing an elaborated simp argument isn’t the most elegant, but follows from the code. While I am at it, move handling of `[*]` into `elabSimpArgs`. Downstream adaption branches exist (but may not be fully up to date because of the permission changes). While I am at it, I cleaned up `SimpTheorems.lean` file a bit (sorting declarations, mild renaming) and added documentation.
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.
This PR refactors the way simp arguments are elaborated: Instead of changing the `SimpTheorems` structure as we go, this elaborates each argument to a more declarative description of what it does, and then apply those. This enables more interesting checks of simp arguments that need to happen in the context of the eventually constructed simp context (the checks in leanprover#8688), or after simp has run (unused argument linter leanprover#8901). The new data structure describing an elaborated simp argument isn’t the most elegant, but follows from the code. While I am at it, move handling of `[*]` into `elabSimpArgs`. Downstream adaption branches exist (but may not be fully up to date because of the permission changes). While I am at it, I cleaned up `SimpTheorems.lean` file a bit (sorting declarations, mild renaming) and added documentation.
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 leanprover#5111. In the end, this implemented much simpler logic than described there (and tried in the abandoned leanprover#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.
This PR adds the
linter.simp.loopProtectionoption (on by default) which makessimpdetect and warn about some obviously looping rewrite rules, implementing the ideas in #5111.This check is not complete (will not detect all possible loops) and not conservative (will warn about theorems that could be looping, even if they are not looping in the simp application at hand, e.g. because of the concrete terms some other rewrite rule breaks the loop). Nevertheless is should, in practice, alert the user about likely unintended actions.
The warning triggers when a rewrite rule applies to its own (abstract) right-hand side, a pair of rewrite rules applies to each other’s right-hand side (or a larger cycle), and one of these rewrite rules is about to be actually applied by simp. It ignores local hypotheses and simp theorems that are marked as permuting.
For now it only checks explicit
simp arguments, but not lemmas added to the simp set via@[simp]`, on the assumption that ad-hoc simp theorems are most likely the cause of loops.The check can be disabled with
set_option linter.simp.loopProtection false, as usual.The mathlib adaption branch simply sets that option globally for mathlib for an easy transition. Partial fixes to make mathlib clean wrt to this setting can be found in leanprover-community/mathlib4#26032, if they are useful to anyone.
Here are some patterns of working
simpcalls found in mathlib that will trigger this:Injectivity lemmas applied in reverse:
where
dual_injshowsM₁✶ = M₂✶ ↔ M₁ = M₂. Clearly its inverse is not terminating. It works here because some other lemmas kick in for concreteM₁✶before it can add another start. This can be made more explicit by writing insteadReversing duplicate lemmas
The
mem_coehere isAffineSubspace.mem_coe. There is alsoSetLike.mem_coein the simp set, so they will conflict now. This can be fixed by disabling the otheror writing
Known deficiencies of the initial implementation:
@[simp]attribution application time.- foofor a simp theorem).simp [f, g_eq_f])f.eq_1means.simpcould suggest turnig it on in the “maximum recursion depth has been reached” message (e.g. to benefit mathlib)simp -loopProtection …instead ofset_option linter.simp.loopProtection false in simp …. This is plausible, but needs some preparation to allow good interaction between options and simp config flags.