3

I'm surprised to see the following function fail the termination check. y ∷ ys is structurally smaller than x ∷ y ∷ ys, isn't it?

open import Data.List using (List ; [] ; _∷_)
open import Data.Nat using (ℕ ; zero ; suc)
open import Data.Nat.Properties using (<-cmp)

foo : List ℕ → ℕ
foo [] = 0
foo (x ∷ []) = 1
foo (x ∷ y ∷ ys) with <-cmp x y
... | _ = suc (foo (y ∷ ys))

Doing either (or both) of the following two things seems to make the termination checker see the light:

  • Removing the with-abstraction.

  • Changing the last clause to match with y ∷ ys instead of x ∷ y ∷ ys and recurse with ys instead of y ∷ ys. (And also changing <-cmp x y to <-cmp y y for a lack of xs.)

Now I am even more confused than I usually am and I'm wondering: What's going on, how does the with-abstraction (and its helper function) factor into all of this, and what do I do about it?

I've seen the other questions and answers concerning termination, but - unlike those more complicated cases - the case at hand seems to be about basic structural recursion, no?

Update

I just found an answer to the question, but if anybody would like to shed more light on what exactly is going on, e.g., how exactly the with-abstraction interferes with termination checking, then I'd be more than happy to accept that answer instead.

123omnomnom
  • 294
  • 1
  • 10
  • 1
    This is quite strange indeed ... especially when noticing that the wiki page on termination (https://agda.readthedocs.io/en/v2.6.1/language/termination-checking.html) has a section named "with-functions" which happens to be empty. I'm gonna make some more tests to see if this behavior can somewhat be understood. – MrO Jul 03 '20 at 21:01
  • 1
    It does look like a bug. You could use the `{-# TERMINATING #-}` pragma before your definition to avoid having this warning, even though it only hides the issue. If nobody else finds a reason here, you could file a bug report. – MrO Jul 03 '20 at 21:13
  • 1
    Thank you very much for looking into this! I also did some more research and stumbled across a note in the 2.6.1 CHANGELOG.md file, which points out that this is a known limitation of the termination checker and provides a workaround. I'll write an answer. – 123omnomnom Jul 03 '20 at 21:56
  • I'm very curious to know what could possibly cause such a limitation. – MrO Jul 03 '20 at 22:00
  • I am, too! Maybe somebody else will weigh in. Thanks again for thinking through this. – 123omnomnom Jul 03 '20 at 22:17

1 Answers1

3

Turns out that this is a known limitation of the termination checker since 2.6.1. See the Termination checking section in the change log for 2.6.1: https://github.com/agda/agda/blob/v2.6.1/CHANGELOG.md

The pattern match and the recursive call won't work with a with-abstraction between them. A workaround is to also abstract over the recursive call in order to pull it up into the with-abstraction (from its original location after the with-abstraction).

open import Data.List using (List ; [] ; _∷_)
open import Data.Nat using (ℕ ; zero ; suc)
open import Data.Nat.Properties using (<-cmp)

foo : List ℕ → ℕ
foo [] = 0
foo (x ∷ []) = 1
foo (x ∷ y ∷ ys) with foo (y ∷ ys) | <-cmp x y
... | rec | _ = suc rec

In the above code, the pattern match x ∷ y ∷ ys and the recursive call foo (y ∷ ys) don't straddle the with-abstraction any longer and the termination check succeeds.

The above fixes my issue, but the change log describes more delicate cases that require a little more care.

This issue is tracked in Agda issue #59 (!), which contains more details and a history of the problem: https://github.com/agda/agda/issues/59

123omnomnom
  • 294
  • 1
  • 10