Skip to content

Inconsistent pattern-match warnings when using guards versus case expressions

Consider the following code (apologies for the rather non-minimal example):

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE EmptyCase #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wall -Wno-unticked-promoted-constructors #-}
module Bug where

import Data.Kind
import Data.Type.Bool
import Data.Type.Equality ((:~:)(..))
import Data.Void

data family Sing :: forall k. k -> Type
data instance Sing :: Bool -> Type where
  SFalse :: Sing False
  STrue  :: Sing True
data instance Sing :: forall a. [a] -> Type where
  SNil  :: Sing '[]
  SCons :: Sing x -> Sing xs -> Sing (x:xs)
data instance Sing :: forall a b. (a, b) -> Type where
  STuple2 :: Sing x -> Sing y -> Sing '(x, y)
newtype instance Sing (f :: k1 ~> k2) =
  SLambda { (@@) :: forall t. Sing t -> Sing (f @@ t) }

data TyFun :: Type -> Type -> Type
type a ~> b = TyFun a b -> Type
infixr 0 ~>
type family (f :: k1 ~> k2) @@ (x :: k1) :: k2
infixl 9 @@

newtype Map k v = MkMap [(k, v)]
data instance Sing :: forall k v. Map k v -> Type where
  SMkMap :: Sing x -> Sing (MkMap x)

type family MapEmpty :: Map k v where
  MapEmpty = MkMap '[]

type family MapInsertWith (f :: v ~> v ~> v) (new_k :: k) (new_v :: v) (m :: Map k v) :: Map k v where
  MapInsertWith _ new_k new_v (MkMap '[]) = MkMap '[ '(new_k, new_v)]
  MapInsertWith f new_k new_v (MkMap ('(old_k,old_v):old_kvs)) =
    If (old_k == new_k)
       (MkMap ('(new_k,f @@ new_v @@ old_v):old_kvs))
       (Case (MapInsertWith f new_k new_v (MkMap old_kvs)) old_k old_v)

type family Case (m :: Map k v) (old_k :: k) (old_v :: v) :: Map k v where
  Case (MkMap kvs) old_k old_v = MkMap ('(old_k,old_v) : kvs)

sMapInsertWith :: forall k v (f :: v ~> v ~> v) (new_k :: k) (new_v :: v) (m :: Map k v).
                  SEq k
               => Sing f -> Sing new_k -> Sing new_v -> Sing m
               -> Sing (MapInsertWith f new_k new_v m)
sMapInsertWith _  snew_k snew_v (SMkMap SNil) = SMkMap (STuple2 snew_k snew_v `SCons` SNil)
sMapInsertWith sf snew_k snew_v (SMkMap (STuple2 sold_k sold_v `SCons` sold_kvs)) =
  case sold_k %== snew_k of
    STrue -> SMkMap (STuple2 snew_k (sf @@ snew_v @@ sold_v) `SCons` sold_kvs)
    SFalse ->
      case sMapInsertWith sf snew_k snew_v (SMkMap sold_kvs) of
        SMkMap skvs -> SMkMap (STuple2 sold_k sold_v `SCons` skvs)

class PEq a where
  type (x :: a) == (y :: a) :: Bool
class SEq a where
  (%==) :: forall (x :: a) (y :: a).
           Sing x -> Sing y -> Sing (x == y)

mapInsertWithNonEmpty1 :: forall k v (f :: v ~> v ~> v) (old_k :: k) (old_v :: v) (old_kvs :: [(k,v)])
                                     (new_k :: k) (new_v :: v) (m :: Map k v).
                          SEq k
                       => Sing f -> Sing new_k -> Sing new_v -> Sing m
                       -> m :~: MkMap ('(old_k,old_v) : old_kvs)
                       -> MapInsertWith f new_k new_v m :~: MapEmpty
                       -> Void
mapInsertWithNonEmpty1 sf snew_k snew_v (SMkMap sm) Refl Refl
  | STuple2 sold_k _ `SCons` sold_kvs <- sm
  , SFalse <- sold_k %== snew_k
  = case sMapInsertWith sf snew_k snew_v (SMkMap sold_kvs) of {}

mapInsertWithNonEmpty2 :: forall k v (f :: v ~> v ~> v) (old_k :: k) (old_v :: v) (old_kvs :: [(k,v)])
                                     (new_k :: k) (new_v :: v) (m :: Map k v).
                          SEq k
                       => Sing f -> Sing new_k -> Sing new_v -> Sing m
                       -> m :~: MkMap ('(old_k,old_v) : old_kvs)
                       -> MapInsertWith f new_k new_v m :~: MapEmpty
                       -> Void
mapInsertWithNonEmpty2 sf snew_k snew_v (SMkMap sm) Refl Refl
  | STuple2 sold_k _ `SCons` sold_kvs <- sm
  = case sold_k %== snew_k of
      SFalse ->
        case sMapInsertWith sf snew_k snew_v (SMkMap sold_kvs) of {}

If you compile this with GHC 8.6.1 or later, you'll notice something interesting happening with the pattern-match coverage checker warnings:

$ /opt/ghc/8.6.1/bin/ghci Bug.hs
GHCi, version 8.6.1: http://www.haskell.org/ghc/  :? for help
Loaded GHCi configuration from /home/rgscott/.ghci
[1 of 1] Compiling Bug              ( Bug.hs, interpreted )

Bug.hs:78:1: warning: [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In an equation for ‘mapInsertWithNonEmpty1’:
        Patterns not matched: _ _ _ (SMkMap _) Refl Refl
   |
78 | mapInsertWithNonEmpty1 sf snew_k snew_v (SMkMap sm) Refl Refl
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...
Ok, one module loaded.

mapInsertWithNonEmpty1 is deemed non-exhaustive, but mapInsertWithNonEmpty2 is deemed exhaustive. However, the code for the two functions are functionally identical—the only difference is that mapInsertWithNonEmpty1 uses one more pattern guard than mapInsertWithNonEmpty2 does.

Trac metadata
Trac field Value
Version 8.6.1
Type Bug
TypeOfFailure OtherFailure
Priority normal
Resolution Unresolved
Component Compiler
Test case
Differential revisions
BlockedBy
Related
Blocking
CC
Operating system
Architecture
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information