Zulip Chat Archive
Stream: lean4
Topic: Simp not simplifying
Michael Jam (May 06 2021 at 15:38):
Hello, I have this "efficient" function that given a nat, computes true.
def f : Nat → Bool
| 0 => true
| n + 1 =>
(match n with
| 0 => true
| _ + 1 => true
)
&& f n
Now i'm trying to prove the important math theorem: f 1 = true
theorem p : f 1 := by
simp [f]
admit
My issue is that simp does not simplify (in lean4 nightly-2021-05-06) even when being specific about f
I'm learning lean so I'm not very sure, but I expected that simp would replace f 1 with its computed result: true in the goal state.
Maybe the tactic is not smart enough, but is there a way to force f 1 to unwrap itself in a single step?
Kevin Buzzard (May 06 2021 at 16:29):
refl
?
Patrick Massot (May 06 2021 at 16:29):
refl
is called rfl
in Lean 4.
Kevin Buzzard (May 06 2021 at 16:30):
I am not too sure that I expect simp
to work here. You have defined no simp
lemmas and so this will just unfold to some f.rec term which the simplifier probably won't know what to do with? I'm not 100% sure of what I'm talking about though
Scott Viteri (May 06 2021 at 16:31):
Are they really the same?
Lean3: theorem hi : "".data = [] := by refl
Lean4: example : "".data = [] := by rfl
<-- doesn't go through
Kevin Buzzard (May 06 2021 at 16:31):
They're not the same no
Kevin Buzzard (May 06 2021 at 16:33):
In lean 4 rfl
only proves a=a. Lean 3's refl
tactic proves R a a
for any reflexive binary relation whose proof of reflexivity has been tagged with the @[refl]
tag. Lean 4 refl
does exist but only in my mathlib port :p
Kevin Buzzard (May 06 2021 at 16:34):
And instead of tagging the refl lemma you have to add it to the list of lemmas which the tactic tries ;-)
Kevin Buzzard (May 06 2021 at 16:35):
@Scott Viteri my answer no longer applies to your post because you changed ==
to =
Scott Viteri (May 06 2021 at 16:35):
sorry copy paste error
Kevin Buzzard (May 06 2021 at 16:36):
I'm not at lean right now so can't check the definition of .data
Scott Viteri (May 06 2021 at 16:37):
Don't want to derail this, I'll make a separate thread
Wojciech Nawrocki (May 06 2021 at 18:34):
Interestingly enough rfl
doesn't work either when using and
in this order (on master
).
def f : Nat → Bool
| 0 => true
| n + 1 => (match n with
| 0 => true
| _ + 1 => true) && f n
def g : Nat → Bool
| 0 => true
| n + 1 => g n && (match n with
| 0 => true
| _ + 1 => true)
#reduce f 1 -- true
example : f 1 = true := rfl -- fails
#reduce g 1 -- true
example : g 1 = true := rfl -- works
Wojciech Nawrocki (May 06 2021 at 18:48):
It works with set_option smartUnfolding false
.
Daniel Selsam (May 06 2021 at 21:04):
This one is subtle. Here are the smart-unfolding lemmas:
def f._sunfold : Nat → Bool :=
fun (x : Nat) =>
match x with
| 0 => idRhs Bool true
| Nat.succ n =>
(match n with
| 0 => idRhs Bool true
| Nat.succ x => idRhs Bool true) &&
f n
def g._sunfold : Nat → Bool :=
fun (x : Nat) =>
match x with
| 0 => idRhs Bool true
| Nat.succ n =>
g n &&
match n with
| 0 => idRhs Bool true
| Nat.succ x => idRhs Bool true
The call f 1
will smart-unfold to f 0
, which recursively smart-unfolds to Bool.true
, which is not wrapped with idRHS
; currently WHNF does not realize that this term is acceptable, and so rejects the smart-unfold. In contrast, g 1
will smart-unfold to idRHS true
, and so true
is accepted.
Mario Carneiro (May 07 2021 at 00:55):
This bug looks similar to https://leanprover.zulipchat.com/#narrow/stream/270676-lean4/topic/Smart.20unfolding.20produces.20ununfoldable.20terms . The idRhs
applications in the match n
seem to be spurious, it should be around the &&
.
Mario Carneiro (May 07 2021 at 01:09):
reported as leanprover/lean4#445
Daniel Selsam (May 07 2021 at 01:11):
@Mario Carneiro Do you know if "smart-unfolding" has a precise semantics?
Mario Carneiro (May 07 2021 at 01:21):
The specification I would propose is that smart unfolding is a transformation on terms like so:
-
If
e
ismatch x with (| pat_i => e_i)*
wherex
is a variable,
thensmart_unfold(e)
ismatch x with (| pat_i => smart_unfold(e_i))*
-
Otherwise,
smart_unfold(e) = idRhs _ e
.
Another possible clause would apply to if statements:
- If
e
isif c then e1 else e2
thensmart_unfold(e)
isif c then smart_unfold(e1) else smart_unfold(e2)
but given how this interacts with the consumer of smart unfolding, namely isDefEq
when it wants to unfold a function, it seems like having it depend on an arbitrary predicate being true will be difficult to control. So I would stick with just top level match (on a variable) being smart unfolded, as in the first two bullets.
Daniel Selsam (May 07 2021 at 01:42):
Are you sure that wouldn't cause simp
to loop?
def foo (n : Nat) : Nat :=
if False then 1000 else
match n with
| 0 => 0
| n+1 => foo n
def foo._proposed_sunfold : Nat → Nat :=
fun (n : Nat) => idRhs $
if False then 1000 else
match n with
| 0 => 0
| Nat.succ n => foo n
Simp on foo n
:
- smart-unfold
foo n
to expose theif
- traverse down to inner
foo n
- repeat
Mario Carneiro (May 07 2021 at 01:45):
Yes, this is a common problem with definitions that don't start with a pattern match. I don't think that is an issue for smart unfolding to solve though
Mario Carneiro (May 07 2021 at 01:47):
You hit this problem in lean 3 when unfolding definitions by well founded recursion because those often don't start with a pattern match, for example gcd
.
Mario Carneiro (May 07 2021 at 01:49):
One potential fix is for simp
to detect simple infinite loops, in this case that a definition is going to unfold unconditionally to a term containing itself, and then handle these definitions specially, either never unfolding them or only unfolding once. In such cases I don't think it is unreasonable to ask the user to use another tactic or another configuration of simp
like single_pass := true
Mario Carneiro (May 07 2021 at 01:52):
For isDefEq
this isn't a major problem since it only unfolds lazily so having a recursive unfolding like this is fine as long as it is "productive" (i.e. doesn't unfold trivially to an application of itself like foo := foo
)
Mario Carneiro (May 07 2021 at 01:53):
(plus these kinds of unfolding aren't definitional anyway so isDefEq
generally doesn't have to worry about them)
Michael Jam (May 26 2021 at 12:59):
Hi. Here is another example:
inductive binary_tree α where
| nil
| node (value : α) (left_subtree : binary_tree α) (right_subtree : binary_tree α)
open binary_tree
def binary_tree_is_full : binary_tree α → Bool
| nil => true
| node _ nil nil => true
| node _ nil _ => false
| node _ _ nil => false
| node _ left_tree right_tree =>
binary_tree_is_full left_tree && binary_tree_is_full right_tree
theorem th
(value : α) (left_tree : binary_tree α) (right_tree : binary_tree α)
(left_tree_is_full : binary_tree_is_full left_tree)
(right_tree_is_full : binary_tree_is_full right_tree)
: binary_tree_is_full (node value left_tree right_tree) := by
simp [binary_tree_is_full]
admit
simp doesn't unfold binary_tree_is_full. I expect it unfolds.
Is this still a bug? I'm using Lean (version 4.0.0-nightly-2021-05-26, commit 02e917793e07, Release)
Sebastian Ullrich (May 26 2021 at 13:13):
What do you expect it to unfold to? The case is not sufficiently determined.
Michael Jam (May 26 2021 at 13:19):
oh i see you're right
Michael Jam (May 26 2021 at 13:31):
What about this:
inductive binary_tree α where
| nil
| node (value : α) (left_subtree : binary_tree α) (right_subtree : binary_tree α)
open binary_tree
def binary_tree_is_full : binary_tree α → Bool
| nil => true
| node _ nil nil => true
| node _ nil _ => false
| node _ _ nil => false
| node _ left_tree right_tree =>
binary_tree_is_full left_tree && binary_tree_is_full right_tree
theorem th
(value : α) (left_tree : binary_tree α) (right_tree : binary_tree α)
(left_tree_is_full : binary_tree_is_full left_tree)
(right_tree_is_full : binary_tree_is_full right_tree)
: binary_tree_is_full (node value left_tree right_tree) := by
let tree := node value left_tree right_tree
match tree with
| nil => admit
| node _ nil nil => admit
| node _ nil _ => admit
| node _ _ nil => admit
| node _ l r =>
simp [binary_tree_is_full]
admit
Shouldn't the last simp unfold to binary_tree_is_full l && binary_tree_is_full r
?
Michael Jam (May 26 2021 at 13:40):
Maybe this unprovable theorem is cleaner:
theorem th1 (tree : binary_tree α)
: binary_tree_is_full tree := by
match tree with
| nil =>
simp [binary_tree_is_full]
| node _ nil nil =>
simp [binary_tree_is_full]
| node _ nil _ =>
simp [binary_tree_is_full]
admit
| node _ _ nil =>
simp [binary_tree_is_full]
admit
| node _ l r =>
simp [binary_tree_is_full]
admit
I match with the same structure as in binary_tree definition, the first simps seem to work but not the last ones
Sebastian Ullrich (May 26 2021 at 13:43):
It's not that simple in presence of overlapping patterns. Your last case will only unfold if it's clear the prior ones did not match, i.e. on node/node
. So you should match on node _ (node ..) (node ..)
in the last proof case.
Sebastian Ullrich (May 26 2021 at 13:45):
Accumulating the knowledge that the prior proof cases did not match either would in theory be possible, but is very much nontrivial. It's also not clear how to actually make that knowledge accessible in general. E.g. if you match first on node _ nil _
and then node ..
, what should the goal state look like?
Michael Jam (May 26 2021 at 13:46):
I see... I think I will try to avoid overlapping patterns now
Michael Jam (May 26 2021 at 13:47):
Thanks
Sebastian Ullrich (May 26 2021 at 13:49):
At least in your proofs, yes :)
Daniel Fabian (May 26 2021 at 14:10):
@Sebastian Ullrich when you talk about "in proofs" are you referring to noConfusion
not working in Sort 0
?
Sebastian Ullrich (May 26 2021 at 14:11):
No, I don't know about any connection to that
Last updated: Dec 20 2023 at 11:08 UTC