leanprover/lean4

GuessLex too clever for the default terminaton_tactic.

nomeata opened this issue · 2 comments

Consider this terminating function

def foo (n m : Nat) : Nat :=
  if h : m = 0 then
    if _ : n = 0 then 0 else foo (n - 1) n
  else
    foo (n - 1) (m - 1)
termination_by?

Here is what happens: GuessLex looks at how n and m decrease in each call, and finds out that (n, m) is a suitable termination measure: In the second call, n decreases non-strictly (≤) and m decreases strictly (<). Because GuessLex looks at how each of these behave separately, this looks promising, and it sets

termination_by (sizeOf n, sizeOf m)

But then the default decreasing_tactic is not able to make use sense of this, because

    repeat (first | apply Prod.Lex.right | apply Prod.Lex.left)

can only handle (<, ?) and (=, <), (with = being defeq), but not (≤, <). When I wrote GuessLex I was hoping to improve the decreasing_tactic behavior along it, but revamping that did not happen yet.

Incidentially, omega has some (incomplete) support for Prod.Lex, that’s why

decreasing_by all_goals simp_wf; omega

Probably GuessLex should not even look for , at least not until the default termination tactic can handle that, as unfortunate as that is.

Versions

4.7.0

Additional Information

Observed on Zulip at https://leanprover.zulipchat.com/#narrow/stream/270676-lean4/topic/brecOn.20application.20type.20mismatch.20during.20termination.20check.3F/near/433118224

Impact

Add 👍 to issues you consider important. If others are impacted by this issue, please ask them to add 👍 to it.

Another possible fix could be

  `(tactic|
    (simp_wf
+   try omega
    repeat (first | apply Prod.Lex.right | apply Prod.Lex.left)
    repeat (first | apply PSigma.Lex.right | apply PSigma.Lex.left)

Not very disciplined, and will run omega twice since it is also part of trivial_decreasing (maybe remove it there then?), but maybe good enough as an interim solution.

Hmm, now that I think about it, I am not sure why I abandoned my attempt at letting decreasing_with handle this correctly. I vaguely remember playing with a variant of decreasing_with that is smarter about this, at the expense of requiring backtracking.

Am I right that Lean does not support backtracking across ;, in the sense that (first | a | b) ; c (or a variant thereof) will try a;c first and if that fails (in a or c), will try b;c, right?

Then probably this needs something like

syntax "solve_lex" tacticSeq : tactic

macro_rules
 | `(tactic| solve_lex $ts:tacticSeq) =>
   `(tactic| first | (apply Prod.Lex.right ; ($ts))
                   | solve | apply Prod.Lex.left
                             ($ts)
                           | apply Prod.Lex.right
                             . $ts
                             . solve_lex $ts
                           | $ts)

(if that is reasonable, with all the recursion and duplication and hopefully correct backtracking) to be used as

macro "decreasing_with " ts:tacticSeq : tactic =>
 `(tactic|
   (simp_wf
    solve_lex (
      first
      | done
      | $ts
      | fail "failed to prove termination, possible solutions:
    - Use `have`-expressions to prove the remaining goals
    - Use `termination_by` to specify a different well-founded relation
    - Use `decreasing_by` to specify your own tactic for discharging this kind of goal")
   )))

I’ll try that the coming week if none beats me to it.