Zulip Chat Archive
Stream: lean4
Topic: termination_by proof with Prod.lex
Floris van Doorn (Sep 04 2025 at 18:54):
In the following code snippet, I want to prove a function to be terminating by giving an explicit measure. I'm quite sure this measure is decreasing, but Lean returns an unprovable goal in one of the cases: it asks me to prove that the measure is decreasing in the first component, but it is decreasing in the second component (and the first component remains the same).
I'm happy to give a manual have-statement, but I don't know how to do that (I tried something in a comment, that doesn't work). I prefer to avoid decreasing_by.
-- some given function
def modifyString : List String → Option String := fun s ↦ some (String.join s)
def myFunction : List String → List String → String
| [], [] => ""
| [], x::s => x ++ myFunction s []
| pre::l, s' =>
let s := s' ++ [pre]
match modifyString s with
| some post => post ++ myFunction l []
| none =>
-- have : WellFoundedRelation.rel (l.length + s.length, l.length) ((pre::l).length + s'.length, (pre::l).length) := by
-- sorry
myFunction l s
-- goal: cannot prove `⊢ l.length + (s'.length + 1) < l.length + 1 + s'.length`
termination_by l s => (l.length + s.length, l.length)
Similar thread here (but without satisfactory answer): #lean4 > termination_by and Prod.Lex
Henrik Böving (Sep 04 2025 at 19:20):
-- some given function
def modifyString : List String → Option String := fun s ↦ some (String.join s)
def myFunction : List String → List String → String
| [], [] => ""
| [], x::s => x ++ myFunction s []
| pre::l, s' =>
let s := s' ++ [pre]
match modifyString s with
| some post => post ++ myFunction l []
| none =>
-- have : WellFoundedRelation.rel (l.length + s.length, l.length) ((pre::l).length + s'.length, (pre::l).length) := by
-- sorry
myFunction l s
-- goal: cannot prove `⊢ l.length + (s'.length + 1) < l.length + 1 + s'.length`
termination_by l s => (l.length + s.length, l.length)
decreasing_by all_goals grind
Henrik Böving (Sep 04 2025 at 19:21):
I think the unprovable goal could be because of some heuristic that is applied to the goal before being passed on to the have mechanism? Maybe that shouldn't be done if the goal isn't trivially provable right away?
CC @Joachim Breitner
Joachim Breitner (Sep 04 2025 at 19:54):
Floris van Doorn schrieb:
I prefer to avoid
decreasing_by.
Why? I find it much more natural than have statements in the function that then end up in the equational of the function, when really the proofs should be relevant only and thus contained to recursion checking.
Joachim Breitner (Sep 04 2025 at 19:58):
Especially with “interesting” lexicographic proofs you probably want manual control over whether to use left or right', so this works:
-- some given function
def modifyString : List String → Option String := fun s ↦ some (String.join s)
def myFunction : List String → List String → String
| [], [] => ""
| [], x::s => x ++ myFunction s []
| pre::l, s' =>
let s := s' ++ [pre]
match modifyString s with
| some post => post ++ myFunction l []
| none =>
-- have : WellFoundedRelation.rel (l.length + s.length, l.length) ((pre::l).length + s'.length, (pre::l).length) := by
-- sorry
myFunction l s
-- goal: cannot prove `⊢ l.length + (s'.length + 1) < l.length + 1 + s'.length`
termination_by l s => (l.length + s.length, l.length)
decreasing_by
· apply Prod.Lex.left
simp
· apply Prod.Lex.left
simp; omega
· apply Prod.Lex.right'
· simp; omega
· simp
Joachim Breitner (Sep 04 2025 at 19:59):
Or, if you have many goals of similar shape, you can automate that using
decreasing_by all_goals solve
| apply Prod.Lex.left <;> (simp <;> omega)
| apply Prod.Lex.right' <;> (simp <;> omega)
```
Joachim Breitner (Sep 04 2025 at 20:00):
Oh, decreasing_by all_goals grind handles this? So hopefully in a future version of lean this will just work by default.
Floris van Doorn (Sep 04 2025 at 21:07):
Joachim Breitner said:
Floris van Doorn schrieb:
I prefer to avoid
decreasing_by.Why? I find it much more natural than
havestatements in the function that then end up in the equational of the function, when really the proofs should be relevant only and thus contained to recursion checking.
Maybe I should start using decreasing_by more. I agree that it is quite nice in this case. It's a bit annoying you'd have to deal with the cases that Lean otherwise does automatically.
I still think that there should always be some have-invocation that should satisfy the termination checker, even when not giving a decreasing_by argument.
Joachim Breitner (Sep 04 2025 at 21:27):
Floris van Doorn schrieb:
I agree that it is quite nice in this case. It's a bit annoying you'd have to deal with the cases that Lean otherwise does automatically.
You could start with
all_goals try solve | decreasing_tactic
to discharge them
Joachim Breitner (Sep 04 2025 at 21:31):
Hmm, oddly, if I have the goal as shown after decreasing_by, then assumption works but decreasing_tactic doesn’t:
-- some given function
def modifyString : List String → Option String := fun s ↦ some (String.join s)
def myFunction : List String → List String → String
| [], [] => ""
| [], x::s => x ++ myFunction s []
| pre::l, s' =>
let s := s' ++ [pre]
match modifyString s with
| some post => post ++ myFunction l []
| none =>
-- have : WellFoundedRelation.rel (l.length + s.length, l.length) ((pre::l).length + s'.length, (pre::l).length) := by
-- sorry
have : Prod.Lex (fun x1 x2 => x1 < x2) (fun x1 x2 => x1 < x2) (l.length + (s' ++ [pre]).length, l.length)
((pre :: l).length + s'.length, (pre :: l).length) := sorry
myFunction l s
-- goal: cannot prove `⊢ l.length + (s'.length + 1) < l.length + 1 + s'.length`
termination_by l s => (l.length + s.length, l.length)
decreasing_by
all_goals try solve | decreasing_tactic
assumption
Ah, that’s because decreasing_tactic first tries to use lexicographic ordering before trying assumption (https://github.com/leanprover/lean4/blob/ad1a017949674a947f0d6794cbf7130d642c6530/src/Init/WFTactics.lean#L80-L81). That is indeed a bit unfortunate.
Last updated: Dec 20 2025 at 21:32 UTC