Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type-based termination checker #7152

Open
wants to merge 68 commits into
base: master
Choose a base branch
from
Open

Conversation

knisht
Copy link
Contributor

@knisht knisht commented Feb 27, 2024

This is a technical description of the pull request.
For intro-level overview of type-based termination, consider reading user manual.
For deep semantical discussion of the process, consider reading a paper.

The development is pretty much ready to play with, and I welcome whatever tricky examples (including mutual induction-coinduction) you have.

Overview

This PR introduces three options: --type-based-termination (disabled by default), --syntax-based-termination (enabled by default), --size-preservation (enabled by default).

If a module is type-checked with --type-based-termination, the process of encoding starts for all provided definitions. Encoding must run after polarity checking, and currently it is invoked at the beginning of the termination checking. During encoding, internal types for all definitions are converted to sized types (do not confuse them with the existing sized types of Agda; starting from now, the expression sized types refers to a separate concept local to this PR), which form a variant of System Fω. Sized types are stored alongside constants and can be serialized.

If termination checking runs for a function definition under --type-based-termination, then an alternative (to usual structural recursion) termination certificate may be produced. Each clause of the function definition is bidirectionally type-checked against the encoded sized type, where each recursive call gives rise to a CallMatrix based on relations between sized types. As in the classic termination checker, size-change principle ensures that the function is strongly normalizing by the set of matrices.

There is also a feature of size preservation, which is regulated by --size-preservation. The implementation is roughly the following: in a function signature, each output size is attempted to be identified with each input size, obtaining a modified sized type. If the function can still be type-checked against the modified sized type, then the input and output sizes are actually the same and this information is serialized later.

There should not be problematic interaction with any other feature, as in the current implementation, sized types "live in their own world", which makes the whole process similar to the double checker. Additionally, the algorithm fails if it cannot understand unusual behavior (such as pattern-matching in the presence of univalence), so it would rather not work than produce a strange termination certificate.

Performance

The measurements were taken on MacBook M2.

On the standard library, the time measurement produces the following results:

Total                                 275,005ms                 
Typing                                 15,200ms (99,964ms)
TypeBasedTermination                      305ms  (5,639ms)
TypeBasedTermination.SizeTypeChecking   2,735ms           
TypeBasedTermination.SizeGraphSolving   1,060ms           
TypeBasedTermination.SizeTypeEncoding   1,035ms           
TypeBasedTermination.PatternRigids        283ms           
TypeBasedTermination.Preservation         119ms           
TypeBasedTermination.Matrix               100ms           
Termination                               377ms  (2,617ms)
Termination.RecCheck                    2,166ms           
Termination.Compare                        73ms                  

Without the type-based termination, the measurement for termination checking is 3,530ms.

On graded type theory, the results are of similar proportion:

Total                                 1,430,003ms            
Typing                                   60,738ms (742,448ms)                 
TypeBasedTermination                        120ms  (34,736ms)
TypeBasedTermination.Matrix              12,595ms            
TypeBasedTermination.SizeTypeChecking     9,050ms            
TypeBasedTermination.SizeGraphSolving     8,385ms            
TypeBasedTermination.SizeTypeEncoding     4,029ms            
TypeBasedTermination.PatternRigids          542ms            
TypeBasedTermination.Preservation            13ms                
Termination                                 829ms   (4,813ms)
Termination.RecCheck                      2,878ms            
Termination.Compare                         794ms            
Termination.Graph                           310ms            

Without the type-based termination, the measurement for termination checking is 31,342ms.

One important note is that for graded type theory I had to disable size preservation, which is currently implemented very naïvely. This is an area of future work.

Overall, performance impact is acceptable -- on large projects, matrix solving starts taking most of the time, but the syntax-based termination checker would also suffer from it. I suggest that, in the absence of issues with soundness, the type-based termination checker may be enabled by default in the future releases.

@knisht knisht added termination Issues relating to the termination checker pr: squash-me This PR needs squashing labels Feb 27, 2024
@knisht knisht added this to the 2.7.0 milestone Feb 27, 2024
@knisht
Copy link
Contributor Author

knisht commented Feb 29, 2024

Regarding #1209: my termination checker does not permit these functions.

For example, in #1209 (comment), the problematic function looks like

inh : Stream D
force inh = lim inh , inh

inh inside lim inh prevents passing the termination check, because lim : Stream D → D is a constructor accepting a stream of infinite depth, which means that inh should also be at least (due to contravariance) an infinite stream.

@cmcmA20
Copy link
Contributor

cmcmA20 commented Mar 14, 2024

Can you also elaborate on interactions with --cubical, --erased-cubical and --erased-matches?
I couldn't manage to reproduce #5910 with --type-based-termination + --no-syntax-based-termination.

@knisht
Copy link
Contributor Author

knisht commented Mar 14, 2024

I am not aware of any problems connected with --cubical. The core principle here is that my checker tries to resemble System F which it is based on, and if something strange starts happening (like univalence), it fails to proceed. It also means that if there is a terminating function that involves univalence and is accepted by the syntax-based checker, then this function would likely be rejected by the type-based checker.

As for #5910: the pattern-matching on <-> unifies two types, which is quite difficult to fit into System F.

My termination checker also correctly rejects #3883 (comment), because it does not consider Bad as a recursive datatype, hence b is not a size-increasing constructor.

Thank you for the comments, I think it makes sense to also clarify them in the user manual.

@andreasabel
Copy link
Member

Discussion Agda dev meeting 2024-03-24.

  • @knisht will push his latest changes
  • some parts of this PR should be reviewed: documentation, test cases
  • merge it into master so we can easily test it before 2.7.0

Copy link
Member

@jespercockx jespercockx left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you again for all the work. I reviewed the changelog and the user manual, as requested.

CHANGELOG.md Outdated Show resolved Hide resolved
CHANGELOG.md Outdated
@@ -56,6 +56,26 @@ Additions to the Agda syntax.
As in a `with`, multiple bindings can be separated by a `|`, and variables to
the left are in scope in bindings to the right.

* Type-based termination checker

Agda is now able to understand polymorphic functions during checking for structural recursion.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me what it means to "understand polymorphic functions", could you give a concrete example of what's the benefit here (apart from size preservation)?.

qsort cmp (cons x xs) = qsort cmp (filter (cmp x) xs) ++ cons x (qsort cmp (filter (λ y → cmp y x) xs))
```

Type-based termination checking also works for coinduction, which improves the guardedness predicate.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, one minimal example of something that used to fail would help here.

@@ -1,4 +1,4 @@
{-# OPTIONS --cubical-compatible --safe --no-sized-types --no-guardedness --level-universe #-}
{-# OPTIONS --cubical-compatible --safe --no-sized-types --no-guardedness --level-universe --type-based-termination #-}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type-based termination is not necessary for checking the builtin files, so why did you add the flag here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type-based termination checker needs to preprocess existing datatypes before using them in the actual checking process. If there is no flag, the preprocessing does not happen.

This is a behavior I would like to change: it is easy to forget to provide the flag for some definition, and then this definition will be useless for the checker. The preprocessing itself is very cheap and syntax-based, so it might make sense to enable it by default for all files. The only drawback I see here is that the preprocessing may be unstable since it is new.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another solution is to make --type-based-termination coinfective, which I don't like at all.

src/full/Agda/Interaction/Options/Base.hs Outdated Show resolved Hide resolved

Following our intuition with coinductive functions, the are three depth parameters ``k < m < n``, where the outer stream is of depth ``n``, and to pass checking the third clause should return the stream of depth at least ``k``. If the first inner ``fib`` is used with the depth ``k``, and the second ``fib`` is used with the depth ``m`` (note, that the smallest available depth for ``fib .tail`` is ``k``, hence ``fib`` must use something bigger, which is ``m``), then the size-preserving ``zipWith`` returns a stream of size ``k``, which is indeed what is required from it. Now we see that both recursive calls to ``fib`` are performed with depths ``k`` and ``m``, which are smaller than ``n``. Agda concludes that this function is terminating.

Size preservation is tightly coupled with polarities. Given a function signature, all occurrences of *negative inductive* and *positive coinductive* variables are considered as input, and they serve as possible candidates for size preservation analysis. On the other hand, all *positive inductive* and *negative coinductive* variables are considered as output, and a function signature may be size-preserving precisely in them. For example, consider the following definition:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please expand on what you mean with negative inductive variables (etc). I know what's a negative or positive position in a type, but what does it mean for a variable to be negative or positive? Likewise, what does it mean for a variable to be inductive or coinductive?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, "variable" here is a bit of an abstraction leak, what I mean by a variable is actually a datatype. Yes, negativity and positivity refer to positions.




Here, the first ``Nat`` in ``foo`` is in a doubly-negative position, which means that the position is positive, and ``foo`` can be size-preserving in the first ``Nat``. From the definition we see that it is indeed the case. One application of this fact is that the following function passes termination check:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it even mean for foo to be "size-preserving in the first Nat"? It does not produce a Nat as an output, so how could it preserve the size of it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function does produce Nat as an output, the only weird thing here is that the output is in a positive position, not in a strictly positive one. As a user of foo, you can obtain this first Nat, as it is supplied by foo. On the other hand, the second Nat has to be supplied by you, and it is foo which obtains it.

Limitations
-----------

The most significant limitation of the current implementation is rooted in the fact that the size type system relies on System Fω, while the target language of Agda is dependently-typed. In cases where a type signature of a function involves large elimination, it is likely that the type-based termination checker will encounter difficulties. This limitation arises because dependent types introduce additional complexity to the underlying theory, which was initially developed for a variant of System Fω. Further details on the semantical framework can be explored in :ref:`[3]<type-based-termination-checking-references>`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you give an example here of a situation where this problem actually shows up?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't mentioning large elimination enough? We already have an instance of this in the standard library, the reflection modules abuse large elimination, and I cannot check termination of them because of this fact.

@nad
Copy link
Contributor

nad commented Apr 17, 2024

The following code triggers an internal error:

{-# OPTIONS --type-based-termination #-}

open import Agda.Primitive

postulate
  A : Set
  a : A
  P : (A : Set)  A  Set

data D (_ : A) : Set where

module _ (_ _ : D a) where

  record R (a : Level) (A : Set a) : Set a where
    field
      f : A  A

  postulate
    g : (A : Set)  R lzero A

  module M (a : Level) (A : Set a) (r : R a A) where

    h : A  A
    h = R.f r

  i : (A : Set) (r : R lzero A) (x : A) 
      let open M lzero A r in
      R lzero (P A (h x))
  i A r x = g (P A _)
An internal error has occurred. Please report this as a bug.
Location of the error: __IMPOSSIBLE__, called at src/full/Agda/Termination/TypeBased/Checking.hs:427:3 in Agda-2.6.5-8FQ0Lx3GX7VBaPx0Ph3PS5:Agda.Termination.TypeBased.Checking

Sized inference: improve handling of parameterized types

Add inference of size-preservation

Handle mutual recursion

Support size preservation for coinductive types

Allow interaction of sized and non-sized definitions

Fix tests

Preload sizes for each agda file

Allow run only named subset of tests

Extract sized primitives to a separate module to avoid conficts with --sized-types

fixup makefile

Add initial tests for sized inference

Move instrumentation with sized arguments to internal syntax

Invoke size solver on signatures of functions

Support generalized variables

add initial attempt to infer sizes as part of termination

Add support for type generalizations

Properly support system F

Improve support of sized inference

Adapt standard library to sized typing

Further imprve termination check

tmp

use new size solver

Rewrite size solver

Improve large part of standard library

tmp2

Fix handling generics in new AST

Improve coverage of standard library

Introduce generics with arities

Improve handling of projections

fully fix standard library

Add encoding of axioms

Support coinduction

Support coinduction in stdlib

Support inductive size preservation

Support coinductive size preservation

Fix standard library after size preservation

Fix whitespace

Allow running type-based and syntax-based termination checkers separately

Mark builtin files as type-base-terminating

Disperse implementation across several modules

Refactor bidirectional checking part

Refactor encoding fragment

Refactor graph

Refactor patterns and preservation sections

Refactor definitions

Remove leftovers

Revert residuals 2.0

Cleanup accidental indentation

Fix whitespace in tests
@knisht
Copy link
Contributor Author

knisht commented Apr 20, 2024

@nad I fixed this problem

I also enabled preprocessing by default, as was discussed during the Agda dev meeting.
The slowdown on the standard library is negligible: on my machine, it is 1 second out of 303 (see TypeBasedTermination.SizeTypeEncoding ). The preprocessing can be configured by --type-based-termination-encoding.

Total                                   303,124ms            
Miscellaneous                             1,924ms            
Typing                                   18,372ms (105,140ms)
Typing.CheckRHS                          28,818ms            
Typing.CheckLHS                           7,994ms  (27,050ms)
Typing.CheckLHS.UnifyIndices             19,055ms            
Typing.Generalize                        12,676ms            
Typing.TypeSig                            8,029ms            
Typing.OccursCheck                        6,805ms            
Typing.With                               2,915ms            
Typing.InstanceSearch                        64ms     (471ms)
Typing.InstanceSearch.FilterCandidates      302ms            
Typing.InstanceSearch.InitialCandidates     105ms            
Scoping                                   2,883ms  (51,464ms)
Scoping.InverseScopeLookup               48,580ms            
Coverage                                 20,368ms  (39,930ms)
Coverage.UnifyIndices                    19,561ms            
Serialization                            26,461ms  (36,712ms)
Serialization.BinaryEncode                4,388ms            
Serialization.Compress                    3,995ms            
Serialization.Sort                        1,250ms            
Serialization.BuildInterface                616ms            
DeadCode                                    164ms  (21,265ms)
DeadCode.DeadCodeInstantiateFull         19,994ms            
DeadCode.DeadCodeReachable                1,106ms            
Parsing                                   2,240ms  (16,202ms)
Parsing.OperatorsExpr                    11,540ms            
Parsing.OperatorsPattern                  2,421ms            
Deserialization                           8,803ms  (12,384ms)
Deserialization.Compaction                3,580ms            
Positivity                               10,650ms            
Termination                                 895ms   (3,559ms)
Termination.RecCheck                      2,197ms            
Termination.Compare                         313ms            
Termination.Graph                           153ms            
Highlighting                              1,757ms            
TypeBasedTermination                          0ms   (1,077ms)
TypeBasedTermination.SizeTypeEncoding     1,077ms            
Injectivity                                 489ms            
Import                                      447ms            
ProjectionLikeness                          117ms 

@nad
Copy link
Contributor

nad commented Apr 22, 2024

Here is another problem:

{-# OPTIONS --type-based-termination --no-syntax-based-termination #-}

postulate
  P : (A : Set)  A  Set
  f : (A : Set) (x : A)  P A x
  A : Set

module M (_ : Set₁) where

  record R₁ (F : Set  Set) : Set₁ where
    field
      x : F A
      p : P (F A) x

open M Set

open R₁ ⦃ … ⦄ public

record R₂ (F : Set  Set) (A : Set) : Set where
  constructor c
  field
    y : F A

open R₂

postulate
  F : Set  Set

  instance
    r₁ : R₁ F

r₂ : R₁ (R₂ F)
R₁.x r₂ .y = R₁.x r₁
R₁.p r₂    = f (R₂ F A) (c x)
[…]/Bug.agda:32,1-34,30
Termination checking failed for the following functions:
Problematic calls:

The error message could be better.

Here is a variant of the code:

{-# OPTIONS --type-based-termination --no-syntax-based-termination #-}

postulate
  P : (A : Set)  A  Set
  f : (A : Set) (x : A)  P A x
  A : Set

record R₁ (F : Set  Set) : Set₁ where
  field
    x : F A
    p : P (F A) x

open R₁ ⦃ … ⦄ public

record R₂ (F : Set  Set) (A : Set) : Set where
  constructor c
  field
    y : F A

open R₂

postulate
  F : Set  Set

  instance
    r₁ : R₁ F

r₂ : R₁ (R₂ F)
R₁.x r₂ .y = R₁.x r₁
R₁.p r₂    = f (R₂ F A) (c x)

Error message:

[…]/Bug.agda:28,1-30,30
Termination checking failed for the following functions:
  r₂
Problematic calls:
  R₁.x r₂
    (at […]/Bug.agda:30,6-8)

This code is accepted when the regular termination checker is used. Is it expected that the new checker rejects it?

@nad
Copy link
Contributor

nad commented Apr 22, 2024

Here is another problem:

{-# OPTIONS --type-based-termination #-}

data F (A : Set) : Set where
  c₁ : A  F A

f : (A B : Set)  (A  B)  F A  B
f _ _ g (c₁ y) = g y

mutual

  data D : Set where
    c₂ : D′  D

  record D′ : Set where
    coinductive
    field
      force : D

_ : F D′  D
_ = f D′ D c₂
Prelude.!!: index too large
CallStack (from HasCallStack):
  error, called at libraries/base/GHC/List.hs:1366:14 in base:GHC.List
  tooLarge, called at libraries/base/GHC/List.hs:1376:50 in base:GHC.List
  !!, called at src/full/Agda/Termination/TypeBased/Monad.hs:347:60 in Agda-[…]:Agda.Termination.TypeBased.Monad

I encountered the problems reported above when trying to type-check code in my library with --type-based-termination and --no-syntax-based-termination. I suggest that you use this library as a test case.

@txa
Copy link

txa commented May 15, 2024

It would be great if type-based termination would also work for cubical agda which means it would need to know about PathP (as a function) but it should also mark hcomp to be size preserving (hopefully this is true unlike transp which isn't). This would mean that commutativity for CoNat is provable without using TERMINATING in the code below.

{-# OPTIONS --cubical --guardedness #-}
open import Cubical.Core.Everything
open import Cubical.Data.Maybe
open import Cubical.Foundations.Prelude

record ℕ∞ : Set where
  constructor zero-suc
  coinductive
  field
    pred∞ : Maybe ℕ∞

open ℕ∞

zero∞ : ℕ∞
pred∞ zero∞ = nothing

suc∞ : ℕ∞  ℕ∞
pred∞ (suc∞ m) = just m

_+∞_ : ℕ∞  ℕ∞  ℕ∞
_+∞-maybe_ : Maybe ℕ∞  ℕ∞  Maybe ℕ∞

pred∞ (m +∞ n) = pred∞ m +∞-maybe n

nothing +∞-maybe n = pred∞ n
just m' +∞-maybe n = just (m' +∞ n)

variable l m n : ℕ∞
variable l? m? n? : Maybe ℕ∞ 

rneutr : m ≡ m +∞ zero∞ 

rneutr-maybe : m? ≡ m? +∞-maybe zero∞

pred∞ (rneutr {m = m} i) = rneutr-maybe {m? = pred∞ m} i

rneutr-maybe {m? = nothing} = refl
rneutr-maybe {m? = just m'} i = just (rneutr {m = m'} i)

lneutr : m ≡ zero∞ +∞ m
pred∞ (lneutr {m = m} i) = pred∞ m

{-# TERMINATING #-}
suc-lem : m +∞ (suc∞ n) ≡ suc∞ (m +∞ n)
suc-lem-maybe :  {m n m?}  pred∞ m ≡ m?  m? +∞-maybe suc∞ n ≡ just (m +∞ n)

pred∞ (suc-lem {m = m} {n = n} i) = suc-lem-maybe {m = m}{n = n}{m? = pred∞ m} refl i

suc-lem-maybe {m = m} {n = n} {m? = nothing} q = 
  pred∞ (suc∞ n)
    ≡⟨⟩
  just n
    ≡⟨ cong {B = λ _  Maybe ℕ∞} just lneutr ⟩ 
  just (zero∞ +∞ n)  
    ≡⟨ cong {B = λ _  Maybe ℕ∞} (λ x  just (x +∞ n)) aux ⟩ 
  just (m +∞ n) ∎
  where aux : zero∞ ≡ m
        pred∞ (aux i) = q (~ i)
suc-lem-maybe {m = m} {n = n} {m? = just m'} q =
   cong {B = λ _  Maybe ℕ∞} just (
   ((m' +∞ suc∞ n)
       ≡⟨ suc-lem ⟩
     suc∞ (m' +∞ n)
       ≡⟨ aux ⟩
      m +∞ n ∎))
      where aux : suc∞ (m' +∞ n) ≡ m +∞ n
            pred∞ (aux i) = q (~ i) +∞-maybe n

{-# TERMINATING #-}
comm : m +∞ n ≡ n +∞ m
comm-maybe :  {m n m? n?}  pred∞ m ≡ m?  pred∞ n ≡ n?  (m? +∞-maybe n) ≡ n? +∞-maybe m

pred∞ (comm {m = m} {n = n} i) = comm-maybe {m = m}{n = n}{m? = pred∞ m}{n? = pred∞ n} refl refl  i
comm-maybe {m = m}{n = n}{m? = nothing}{n? = n?} p q =
  pred∞ n
    ≡⟨ rneutr-maybe ⟩
  pred∞ n +∞-maybe zero∞     
    ≡⟨ cong (λ x  pred∞ n +∞-maybe x) aux ⟩
  pred∞ n +∞-maybe m   
    ≡⟨ cong (λ x  x +∞-maybe m) q ⟩
  n? +∞-maybe m ∎
  where aux : zero∞ ≡ m
        pred∞ (aux i) = p (~ i)
comm-maybe {m = m}{n = n}{m? = just m'}{n? = n?} p q  =
  just (m' +∞ n)
    ≡⟨ cong {B = λ _  Maybe ℕ∞} just (comm {m = m'}{n = n}) ⟩  
  just (n +∞ m')
    ≡⟨ sym (suc-lem-maybe {m = n}{n = m'} q) ⟩ 
  (n? +∞-maybe (suc∞ m'))
    ≡⟨ cong (λ x  n? +∞-maybe x) aux ⟩ 
  (n? +∞-maybe m) ∎
  where aux : suc∞ m' ≡ m
        pred∞ (aux i) = p (~ i)

@andreasabel
Copy link
Member

@knisht This PR should be rebased on latest master to updated to the latest CI, but I do not dare to do this as I might break your local copy...

@howtonotwin
Copy link

The following recursion through a type defined by a function does not pass the new checker.

{-# OPTIONS --type-based-termination #-}
module WithTBT where

open import Agda.Builtin.Nat
open import Agda.Builtin.Sigma
open import Agda.Builtin.Unit

Tuple : (A : Set) -> Nat -> Set
Tuple A zero = ⊤
Tuple A (suc n) = Σ A \ _ -> Tuple A n

foldMap : {A : Set} (f : A -> Nat) {n : Nat} -> Tuple A n -> Nat
foldMap f {n = zero} tt = 0
foldMap f {n = suc n} (x , xs) = f x + foldMap f xs

data Tree : Set where
  node : (n : Nat) -> Tuple Tree n -> Tree

tally : Tree -> Nat
tally (node n xs) = n + foldMap tally xs

I can get it to work easily with --sized-types

{-# OPTIONS --sized-types #-}
module WithSized where

open import Agda.Builtin.Nat
open import Agda.Builtin.Sigma
open import Agda.Builtin.Size
open import Agda.Builtin.Unit

Tuple : (A : Set) -> Nat -> Set
Tuple A zero = ⊤
Tuple A (suc n) = Σ A \ _ -> Tuple A n

foldMap : {A : Set} (f : A -> Nat) {n : Nat} -> Tuple A n -> Nat
foldMap f {n = zero} tt = 0
foldMap f {n = suc n} (x , xs) = f x + foldMap f xs

data Tree (i : Size) : Set where
  node : {j : Size< i} (n : Nat) -> Tuple (Tree j) n -> Tree i

tally : {i : Size} -> Tree i -> Nat
tally (node n xs) = n + foldMap tally xs

I don't claim to understand the algorithm, but I think it'd be very nice if it could support this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr: squash-me This PR needs squashing termination Issues relating to the termination checker
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants