Zulip Chat Archive
Stream: lean4
Topic: grind congruence closure fails
Christian Merten (Aug 27 2025 at 11:22):
Is the following grind failure known? (It fails both on latest mathlib and on Lean nightly)
-- works
example (m : Nat) (a b : Nat → Nat) (h : b = a) :
b m = a m := by
grind
-- fails
example (m : Nat) (a : Nat → Nat) (f : (Nat → Nat) → (Nat → Nat)) (h : f a = a) :
f a m = a m := by
grind
(cc succeeds here)
Aaron Liu (Aug 27 2025 at 11:26):
#lean4 > `grind` with partial application
Christian Merten (Aug 27 2025 at 11:30):
The underlying cause might be the same, but this example does not involve any e-matching.
Kim Morrison (Aug 27 2025 at 13:12):
Yes, this is by design.
Christian Merten (Aug 27 2025 at 13:21):
Could you elaborate on why this is expected or is this documented somewhere? I could not find anything related to this in the language reference.
Is there a way to make grind succeed here (by providing some arguments for example)?
Jakub Nowak (Sep 09 2025 at 11:48):
Multiple people already had that problem. :C
I hope this will be addressed. I'm not familiar with grind's code so looking at this myself would be a huge work.
Jakub Nowak (Sep 09 2025 at 14:00):
You can add set_option trade.grind.debug true to see representation of an e-graph:
#0 := False, [val]
#1 := True, [val]
#2 := true, [ctor]
#3 := false, [ctor]
#4 := 0, [val]
#5 := Ordering.eq, [ctor]
#6 := #7 #8 ↦ #8
#7 := f
#8 := a
#9 := @Eq #10 #11 #13 ↦ #0
#10 := Nat
#11 := #7 #8 #12
#12 := m
#13 := #8 #12
{#8, #6}
{#0, #9}
f a m belongs to #11 and f a (I think, not sure) belongs to #6. I don't know why 2-ary application isn't represent as two 1-ary applications. This way, instead of #11 := #7 #8 #12 we would get #11 := #6 #12, and grind would succeed.
Jakub Nowak (Sep 09 2025 at 14:04):
I think it might be implemented this way, because current code isn't able to match patterns across multiple e-nodes. So to be able to match n-ary patterns the whole n-ary expression must be stored in one e-node.
This is probably not a problem, the whole expressions are stored in e-nodes and 1-ary application can always be treated as n-ary.
Jakub Nowak (Sep 09 2025 at 23:28):
https://arxiv.org/pdf/1701.04391
I think this is the paper grind algorithm is based upon? In the paper the proposed congruent procedure is:
procedure congruent(D, E)
(f, a) ← app(D); (g, b) ← app(E)
return a ≈ b and
[(f ≈ g and type(f) ≡ type(g)) or
(f and g are local definitions and congruent(def(f), def(g)))]
And I think that what is currently implemented in lean4 is
procedure congruent(D, E)
(f, a) ← app(D); (g, b) ← app(E)
return a ≈ b and
[if f and g are both applications
then congruent(def(f), def(g))
else f ≈ g and type(f) ≡ type(g)]
I.e. if f and g are applications they're being eagerly unfolded.
Similarly the proposed congrhash procedure:
procedure congrhash(D)
given: `h`, a hash function on terms
(f, a) ← app(D)
return hashcombine(h(repr[f]), h(repr[a]))
is implemented in lean4 as
procedure congrhash(D)
given: `h`, a hash function on terms
(f, a) ← app(D)
if f is application then
return hashcombine(congrhash(f), h(repr[a]))
else
return hashcombine(h(repr[f]), h(repr[a]))
The "local definition" part was replaced with checking if expression is application, which I think is fine. We don't want grind to automatically unfold definitions.
But we shouldn't eagerly unfold applications until reaching head expression. I think that implementing the congruence algorithm as described in the paper would fix the issue.
Jakub Nowak (Dec 04 2025 at 09:44):
Looks like this was fixed in nightly. :tada:
Jakub Nowak (Dec 04 2025 at 09:57):
Or not? It seems like this particular case works, by doesn't in this other example #lean4 > `grind` should unfold projections @ 💬
Jakub Nowak (Dec 04 2025 at 10:00):
Maybe grind learned how to rewrite equalities in head expressions, but still can't match grind patterns when head expression is an e-class?
Théophile (Dec 04 2025 at 11:57):
Relevant PR: https://github.com/leanprover/lean4/pull/11323
Last updated: Dec 20 2025 at 21:32 UTC