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

Coinduction incompatible with univalence [WAS: Structural termination order incompatible with univalence] #1023

Closed
GoogleCodeExporter opened this issue Aug 8, 2015 · 37 comments

Comments

@GoogleCodeExporter
Copy link

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Following discussions on the coq-club it was discovered that Agda's current
--without-K does not guarantee compatibility with univalence. My version of
--without-K DOES rule out the bad use of Refl in the following test case, an
argument for accepting my patch (see issue 865).

-- Andreas, 2014-01-10
-- Code by Jesper Cockx and Conor McBride and folks from the Coq-club

{-# OPTIONS --without-K #-}

-- An empty type.

data Zero : Set where

-- A unit type as W-type.

mutual
  data WOne : Set where wrap : FOne -> WOne
  FOne = Zero -> WOne

-- Type equality.

data _<->_ (X : Set) : Set -> Set where
  Refl : X <-> X

-- This postulate is compatible with univalence:

postulate
  iso : WOne <-> FOne

-- But accepting that is incompatible with univalence:

noo : (X : Set) -> (WOne <-> X) -> X -> Zero
noo .WOne Refl (wrap f) = noo FOne iso f

-- Matching against Refl silently applies the conversion
-- FOne -> WOne to f.  But this conversion corresponds
-- to an application of wrap.  Thus, f, which is really
-- (wrap f), should not be considered a subterm of (wrap f)
-- by the termination checker.
-- At least, if we want to be compatible with univalence.

absurd : Zero
absurd = noo FOne iso (\ ())

Original issue reported on code.google.com by andreas....@gmail.com on 10 Jan 2014 at 8:27

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Attached my version of --without-K (as discussed in issue #865) if you want to try (and break) it.

Original comment by andreas....@gmail.com on 10 Jan 2014 at 8:42

Attachments:

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Here is an adaption of Maxime Denes coinduction version (from the coq list):

open import Common.Coinduction
open import Common.Equality

prop = Set

data False : prop where

data CoFalse : prop where
  CF : False  CoFalse

data Pandora : prop where
  C : ∞ CoFalse  Pandora

postulate
  ext : (CoFalse  Pandora)  (Pandora  CoFalse)  CoFalse ≡ Pandora

out : CoFalse  False
out (CF f) = f

foo : CoFalse ≡ Pandora
foo = ext (λ{ (CF ())               })
          (λ{ (C c)  CF (out (♭ c))})

loop : CoFalse
loop rewrite foo = C (♯ loop)

false : False
false = out loop

Original comment by andreas....@gmail.com on 2 Mar 2014 at 7:15

  • Added labels: Coinduction

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Simplified version:

open import Common.Coinduction
open import Common.Equality

prop = Set

data False : prop where

data Pandora : prop where
  C : ∞ False  Pandora

postulate
  ext : (False  Pandora)  (Pandora  False)  False ≡ Pandora

foo : False ≡ Pandora
foo = ext (λ()) (λ{ (C c)  ♭ c })

loop : False
loop rewrite foo = C (♯ loop)

Original comment by andreas....@gmail.com on 7 Mar 2014 at 2:12

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

The coinduction version doesn't seem to have anything to do with coinduction,
but everything with rewrite. Here's an example without coinduction:

open import Relation.Binary.PropositionalEquality

data Zero : Set where

data WOne : Set where wrap : (Zero  WOne)  WOne

FOne = Zero  WOne

postulate
  iso : WOne ≡ FOne

foo : WOne  Zero
foo (wrap f) rewrite (sym iso) = foo f

BOOM : Zero
BOOM = foo (wrap (λ ()))

It seems that 'rewrite' also relies implicitly on K. Actually this is not very surprising because it doesn't look at the contents of the equality proof, so it just assumes they are all refl.

Original comment by jesper.c...@gmail.com on 18 Mar 2014 at 10:53

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Isn't #4 your original example, which is given in #0?

#3 is about coinduction because a function 

  loop ... = C (♯ loop)

is only accepted by the termination checker because of #, the "coinductive 
constructor".

'rewrite' is just syntactic sugar, internally Agda sees something like this:
open import Common.Equality

data Zero : Set where

mutual
  data WOne : Set where wrap : FOne  WOne
  FOne = Zero  WOne

postulate
  iso : WOne ≡ FOne

foo : WOne  Zero
foo (wrap f) = aux FOne iso f
  where
    aux : (A : Set)  (WOne ≡ A)  A  Zero
    aux .WOne refl f = foo f

BOOM : Zero
BOOM = foo (wrap (λ ()))

Is this rejected by your version of --without-K?

Original comment by andreas....@gmail.com on 18 Mar 2014 at 12:55

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

I guess this needs little more context: I'm trying to fix the problem in #0 by 
modifying the termination checker to only look at arguments whose types are 
actually datatypes. I already have a very experimental version of it (see 
attachment). It catches the original example in #0 and the desugared version of 
foo in #5, but not the examples with 'rewrite' in #2, #3, and #4. So it seems 
that 'rewrite' isn't desugared for the termination check or is desugared to 
something else.

My patch still has a number of problems with copatterns, sized types, 
projections, and pattern synonyms, so I'm not proposing to push it. But maybe 
someone can build on it to create a proper fix.



Original comment by jesper.c...@gmail.com on 18 Mar 2014 at 2:39

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Rewrite is not desugared in the termination checker since the fix of issue #59 (which is a recent one, therefore not yet released officially).

See Agda.Termination.TermCheck:

-- | Extract recursive calls from one clause.
termClause :: Clause -> TerM Calls
termClause clause = do
  name <- terGetCurrent
  ifM (isJust <$> do isWithFunction name) (return mempty) $ do
  mapM' termClause' =<< do liftTCM $ inlineWithClauses name clause

termClause' :: Clause -> TerM Calls

You can simply skip the inlining in your version of --without-K and see if you obtain the desired results then.

Original comment by andreas....@gmail.com on 18 Mar 2014 at 3:36

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Ah yes, thank you for the suggestion. Commenting out termClause and calling 
termClause' directly seems to fix the problem with 'rewrite'. Now all examples 
(#0, #2, #3, #4, and #5) are rejected by the termination checker.

That still leaves the other problems with copatterns, sized types, etc. 

Original comment by jesper.c...@gmail.com on 18 Mar 2014 at 3:58

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

As I see from your patch, you simply mask out all patterns which are not obviously of a data (or record) type. However, this might be too simplistic in the presence of large eliminations, e.g.

T : Bool -> Set
T true = Nat
T false = List Nat

f : (x : Bool) -> T x -> Set
f true zero = Nat
f true (suc x) = f true x
f false nil = Nat
f false (x :: xs) = f false xs

Here, the second argument is of type "T x" which is not detectable as data type without instantiating x.

Maybe we rather want the following: Type check patterns again, but without doing any kind of unification. Only do reduction. (This might actually be a (rather) simple left-to-right traversal through the patterns, because it means that dot-pattern values can never contribute to reducing types.) Every pattern and subpattern which then gets a data or record type is eligible to contribute to termination, the others are not.

We will lose the following function then:

f : (x : Bool) -> T x -> x \equiv false -> Set
f .false zero refl = Nat
f .false (suc n) refl = f false n refl

Is it justified to reject this function with --new-without-K?

Original comment by andreas....@gmail.com on 18 Mar 2014 at 4:54

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

I'm not sure I understand what you mean by "type check patterns again without unification". Can you explain how this would help in the (first) definition of f?

Alternatively, we could define f as

f : (x : Bool) -> T x -> Set
f true = ftrue
  where
    ftrue : Nat -> Set
    ftrue zero = Nat
    ftrue (suc x) = ftrue x
f false = ffalse
  where
    ffalse : List Nat -> Set
    ffalse nil = Nat
    ffalse (x :: xs) = ffalse xs

This is accepted by my patch because it makes it clear to the termination checker that there are actually two separate functions here, and each one is doing recursion on a different type. Maybe there is some way to automatically desugar the original definition of f into this one?

The second definition of f (let's call it g) can also be desugared to

g : (x : Bool) -> T x -> x ≡ true -> Set
g .true n refl = gtrue n
  where
    gtrue : Nat -> Set
    gtrue zero = Nat
    gtrue (suc n) = gtrue n

Original comment by jesper.c...@gmail.com on 19 Mar 2014 at 10:14

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

I meant when you type check say the second clause of f from left to right

  f : (x : Bool) -> T x -> Set
  true : Bool
  ----------------------------------
  f true : T true -> Set
  y : Nat |- (suc y) : Nat = T true
  ---------------------------------------
  y : Nat |- f true (suc y) : Set

you can do completely without unification, and you get out that all patterns
are data type pattern.

For g, without unification, you would derive

  g : (x : Bool) -> T x -> x == true -> Set
  x : Bool |- x : Bool  (dot pattern)
  ----------------------------------------------------
  x : Bool |- g x : T x -> x == true -> Set
  x : Bool, n : T x |- n : T x  (not a data type, pattern marked as unusable)
  --------------------------------------------------
  g x n : x == true -> Set
  x, n |- refl : x == true (fails, pattern marked as unusable)
  -----------------------------------
  x, n |- g x n refl :  Set

Your desugaring can also be applied to foo:

foo : WOne  Zero
foo (wrap f) = aux FOne iso f
  where
    aux : (A : Set)  (WOne ≡ A)  A  Zero
    aux .WOne refl f = aux2 f 
    where 
        aux2 : WOne -> Zero
        aux2 f = foo f

Unless you mark pattern f in aux unusable, you do not reject foo.
But if you do, you should also do so in you desugaring of g, effectively
rejecting g.

Original comment by andreas....@gmail.com on 20 Mar 2014 at 7:22

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

You treatment of g should actually work, I was mistaken.

So you seem to suggest maybe Agda's termination checker should work on a compiled form of pattern matching, something like the case trees, instead. A more or less straightforward translation from deep to shallow pattern matching would turn g into

g : (x : Bool) -> T x -> x ≡ true -> Set
g .true n refl = gtrue n
  where
    gtrue : Nat -> Set
    gtrue zero = Nat
    gtrue (suc n) = g true n refl

To arrive at your form, one would have to "tighten the loop" and see that "g
true n refl" can be simplified to "gtrue n".

Original comment by andreas....@gmail.com on 21 Mar 2014 at 6:08

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

[deleted comment]

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

The main problem with my patch seems to be that it is too restrictive. Since it is better to have a restrictive solution than having none, I propose to enable it anyway for the release of 2.3.4, but only when --without-K is enabled (if you have K then there is no problem). I'm attaching a patch against the current darcs version that does this. This will give us some time to work out a more principled solution (hopefully).

Original comment by jesper.c...@gmail.com on 26 May 2014 at 2:13

Attachments:

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Pushed this. Needs entry in ChangeLog!

Original comment by andreas....@gmail.com on 12 Jun 2014 at 5:05

  • Added labels: Milestone-2.4.0.1

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Original comment by ulf.nor...@gmail.com on 16 Jun 2014 at 11:05

  • Added labels: Milestone-2.4.0.2
  • Removed labels: Milestone-2.4.0.1

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

I guess you know it. Using the patch in #14 and the --without-K option, there 
are several functions in the standard library which don't pass the 
termination-check.

On a personal branch of the standard library using the --without-K option, I needed to add the
{-# NO_TERMINATION_CHECK #-} flag to the following functions

asr/my-agda-stdlib@111816d

Original comment by andres.s...@gmail.com on 19 Jun 2014 at 9:11

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Original comment by andreas....@gmail.com on 30 Jul 2014 at 8:57

  • Added labels: Milestone-2.4.2
  • Removed labels: Milestone-2.4.0.2

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

I'm a little unclear about what the status of this one is. Do we know what needs to be done to the termination checker to make it less restrictive together with --without-K? I'm a little reluctant to tag issues with specific
milestones if they require novel research to fix.

I'll remove the milestone for now.

Original comment by ulf.nor...@gmail.com on 20 Aug 2014 at 9:12

  • Removed labels: Milestone-2.4.2

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

The original issue has been fixed, the remainder is an enhancement request for a more liberal fix.

Original comment by andreas....@gmail.com on 25 Aug 2014 at 2:37

  • Changed title: More liberal termination checker for --without-K [WAS: Structural termination order incompatible with univalence]
  • Added labels: Priority-Medium, Type-Enhancement
  • Removed labels: Priority-High, Type-Defect

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

After fixing issue #1259, the problem with coinduction (comment 3) is back. This problem was only accidentially fixed since the termination checker did something random for with functions and --without-K. (This includes rewrite.) Also, nothing in Jesper's code addresses the guardedness check done for coinduction.

I redeclare this as Defect, please open a new Enhancement issue for making the termination checker more liberal --without-K.

Original comment by andreas....@gmail.com on 28 Aug 2014 at 10:05

  • Changed title: Coinduction incompatible with univalence [WAS: Structural termination order incompatible with univalence]
  • Added labels: Priority-High, Type-Defect
  • Removed labels: Priority-Medium, Type-Enhancement

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Here's an adaptation of (comment 3) without 'with' or 'rewrite' (even without any mutual stuff) that exposes the problem:

{-# OPTIONS --without-K #-}

open import Level
open import Common.Coinduction
open import Common.Equality

prop = Set

data False : prop where

data Pandora : prop where
  C : ∞ False  Pandora

postulate
  ext : (False  Pandora)  (Pandora  False)  False ≡ Pandora

f : False  Pandora
f ()

g : Pandora  False
g (C x) = ♭ x

foo : False ≡ Pandora
foo = ext f g

loop-aux : (A : Set)  A ≡ Pandora  A
loop-aux .Pandora refl = C (♯ (loop-aux False foo))

loop : False
loop = loop-aux False foo

The main problem is that AFAIK there hasn't been done any work yet on coinduction in the presence of univalence. Clearly, we need something stronger than guardedness, but it's not yet clear what it should be. I'll think about it.

Original comment by jesper.c...@gmail.com on 28 Aug 2014 at 12:17

@GoogleCodeExporter
Copy link
Author

@GoogleCodeExporter GoogleCodeExporter commented Aug 8, 2015

Fixed by
be82336

Jesper, maybe you can add (comment 22) to the test suite, in case it is not in yet.

(comment 3) is in there as CoinductionAndUnivalence.agda.

Original comment by andreas....@gmail.com on 28 Aug 2014 at 12:24

  • Changed state: Fixed

@Saizan
Copy link
Contributor

@Saizan Saizan commented Jan 21, 2021

It seems that the check for the original problem can be circumvented by the identity type former as a datatype.

The following proof of false typechecks on Agda version 2.6.2-c857009

{-# OPTIONS --without-K #-}

-- An empty type.

data Zero : Set where

-- A unit type as W-type.

mutual
  data WOne : Set where wrap : FOne -> WOne
  FOne = Zero -> WOne

-- Type equality.

data _<->_ (X : Set) : Set -> Set₁ where
  Refl : X <-> X

-- This postulate is compatible with univalence:

postulate
  iso : WOne <-> FOne

-- But accepting that is incompatible with univalence:

data Id (X : Set) : Set where
  i : X  Id X

noo : (X : Set) -> (WOne <-> X) -> Id X -> Zero
noo .WOne Refl (i (wrap f)) = noo FOne iso (i f)

absurd : Zero
absurd = noo FOne iso (i \ ())

@Saizan Saizan reopened this Jan 21, 2021
@jespercockx
Copy link
Member

@jespercockx jespercockx commented Jan 21, 2021

It seems I do not understand how the termination checker works, as I would not have expected this definition to pass the termination checker. Why is it fine to consider i f as being structurally smaller than i (wrap f)? (EDIT: I mean, I don't even understand why this code is fine --with-K.)

@jespercockx
Copy link
Member

@jespercockx jespercockx commented Jan 21, 2021

I re-checked our papers on pattern matching without K, and I'm pretty sure there's no way this example would be accepted by the (rather simplistic) termination criterion we're using there. So this seems to be caused by insufficient integration between the theories supporting the different features of Agda (in this case: pattern matching and termination checking).

@Saizan
Copy link
Contributor

@Saizan Saizan commented Jan 22, 2021

I was also a bit surprised by i f being smaller, maybe it's size-changing termination?, but we can rework the example to not rely on it:

{-# OPTIONS --without-K #-}

data Zero : Set where

data Id (X : Set) : Set where
  i : X  Id X

mutual
  data WOne : Set where wrap : Id FOne -> WOne
  FOne = Zero -> WOne

data _<->_ (X : Set) : Set -> Set₁ where
  Refl : X <-> X

postulate
  iso : WOne <-> FOne

mutual
  noo : (X : Set) -> (WOne <-> X) -> Id X -> Zero
  noo .WOne Refl (i (wrap f)) = noo FOne iso f

absurd : Zero
absurd = noo FOne iso (i \ ())

@Saizan
Copy link
Contributor

@Saizan Saizan commented Jan 22, 2021

Do we know how this problem was addressed in Coq?

@jespercockx
Copy link
Member

@jespercockx jespercockx commented Jan 22, 2021

I believe in Coq, constructor arguments are marked as either "recursive" or "non-recursive", and the termination checker only accepts recursive calls on arguments that come from a recursive position. In this example the argument of i would not be seen as recursive, so the recursive call would be rejected.

However, adopting such a criterion in Agda might also rule out a lot of "good" use cases, e.g. where one first defines a (non-recursive) functor and then uses a separate fixpoint construction, e.g.

data Maybe (A : Set) : Set where
  just    : A  Maybe A
  nothing : Maybe A

data Nat : Set where
  inj : Maybe Nat  Nat

id : Nat  Nat
id (inj (just x)) = inj (just (id x))
id (inj nothing)  = inj nothing

This is something we probably still want to accept, but (I presume) would be rejected by Coq's approach because the argument to just is not recursive.

@Saizan
Copy link
Contributor

@Saizan Saizan commented Jan 22, 2021

Is their fix for the original problem of this issue also similar to what we do in Agda? Since that definition of noo only matched on wrap whose argument is recursive.

@jespercockx
Copy link
Member

@jespercockx jespercockx commented Jan 22, 2021

I tried porting the example to Coq, but it seems it's not accepted:

Inductive Zero : Prop := .

Inductive Id (X : Type) : Type :=
  | i : X -> Id X.

Inductive One : Type :=
  | wrap : Id (Zero -> One) -> One.

Inductive Equiv (X : Type) : Type -> Type :=
  | Refl : Equiv X X.

Axiom iso : Equiv One (Zero -> One).

Fixpoint noo (X : Type) (eq : Equiv One X) (x : Id X) : Zero :=
  (match eq in Equiv _ X return Id X -> Zero with
  | Refl _ => fun x => 
    match (x : Id One) with
    | i _ o => 
      match o with
      | wrap f => noo (Zero -> One) iso f
      end
    end
  end) x.

Error message: Cannot guess decreasing argument of fix.

I'm not sure what Coq exactly does here to reject the example, it seems that it somehow cares about the first argument of the recursive call to noo.

@jespercockx
Copy link
Member

@jespercockx jespercockx commented Jan 22, 2021

Thinking a bit more about it, the reason why Coq rejects the example is because I had to abstract over x : Id X in order for the pattern match on Refl to have the desired effect. If you don't do the abstraction, then the type of x remains at Id X instead of being specialized to Id One. However, that means the link between the argument f in the recursive call and the original argument x is lost, which means the termination checker rejects the recursive call.

In other words, it seems that the Coq criterion can be summarized as "the type of the argument on which the function is structurally recursive cannot be refined by any pattern matches." Or perhaps a bit more refined: "the type of the argument on which the function is structurally recursive must be informative enough to access the argument used in the recursive call without any refinements from (other) pattern matches."

@jespercockx
Copy link
Member

@jespercockx jespercockx commented Jan 22, 2021

Another way to approach the problem is by saying that we can compute in advance an infinite tree of all valid recursive calls from the type of the argument on which we are recursing. The way we construct this infinite tree is just by repeatedly case splitting on a fresh variable of this type and collecting all the possible recursive arguments generated by this process. Crucially, this should be done using the type before any refinements. Some examples:

  • For the buildin Nat we get the patterns x, suc x, suc (suc x), ..., saying that we can recurse on arguments of arbitrary depth
  • For the definition of Nat as the fixpoint of Maybe we get x, inj (just x), inj (just (inj (just x))`, ..., again allowing us to recurse on arguments of arbitrary depth
  • For the original definition of noo the argument has type X, on which we can do no case splitting so there are no valid recursive calls
  • For Andrea's new definition of noo the argument has type Id X, on which we can match as i y but then we're stuck. So again there are no valid recursive calls.

However, using this process naively to check the validity of recursive calls could be prohibitively expensive. Maybe we can devise a cheaper way to approximate it?

@Saizan
Copy link
Contributor

@Saizan Saizan commented Jan 22, 2021

Oh, I see, I was under the impression Coq also had this problem, but maybe it actually was only discussed on coq-club. Unfortunately I can't seem to find the coq-club thread.

@andreasabel
Copy link
Member

@andreasabel andreasabel commented May 16, 2022

Meta-comment: Please do not reopen issues whose fix has been shipped. Rather, open a new issue linking to the old one:

@andreasabel andreasabel removed this from the 2.6.4 milestone May 16, 2022
@andreasabel andreasabel self-assigned this May 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants