packages feed

c-expr-dsl-0.1.0.0: src/C/Expr/Util/TestEquality.hs

{-# LANGUAGE CPP       #-}
{-# LANGUAGE MagicHash #-}

-- | Utilities for writing 'Eq' instances for GADTs
module C.Expr.Util.TestEquality (
    equals1
  , equals2
  ) where

import Data.GADT.Compare
import Data.Kind
import Data.Type.Equality
import GHC.Exts
import Unsafe.Coerce (unsafeCoerce)

-- $setup
-- >>> :seti -XDataKinds -XGADTs

{-------------------------------------------------------------------------------
  Utilities for writing 'Eq' instances for GADTs
-------------------------------------------------------------------------------}

infixr 4 `equals1`
infixr 4 `equals2`

-- | Check whether two GADT values of type @k ->Type@ are equal.
--
-- If so, also return a proof that the tags were equal.
--
-- NB: this is stricter than 'testEquality', as 'testEquality' is supposed
-- to return @Just Refl@ whenever the tags are equal, even when the values
-- themselves are different.
--
-- NB: Doesn't work for types with type indices not-directly implied by
-- constructor "tags":
--
-- >>> data SBool (b :: Bool) where STrue :: SBool True; SFalse :: SBool False
-- >>> instance Eq (SBool b) where STrue == STrue = True; SFalse == SFalse = False
-- >>> data Foo b = Foo1 (SBool b) | Foo2 (SBool b) deriving Eq
-- >>>  equals1 (Foo1 STrue) (Foo1 SFalse)
-- *** Exception: ...Non-exhaustive patterns in function ==
-- ...
--
-- The GHC generated Eq instance for SBool would have default case, so this example *could* work.
-- If you want to be safe, use 'Data.GADT.Compare.geq' from @some@ package.
--
equals1 :: forall a tag1 tag2.
  ( forall tag. Eq ( a tag )
#if MIN_VERSION_base(4,20,0)
  , forall tag. DataToTag ( a tag )
#endif
  ) => a tag1 -> a tag2 -> Maybe ( tag1 :~: tag2 )
equals1 k1 k2
  | -- Fail-fast: comparing the tag first.
    isTrue# ( dataToTag# k1 ==# dataToTag# k2 )
    -- Assume the types are the same; this allows us to use the 'Eq' instance.
  , Refl <- ( unsafeCoerce Refl :: tag1 :~: tag2 )
  , k1 == k2
  -- The values are equal (according to the 'Eq' instance): this justifies the
  -- unsafe coercion above, assuming that the 'Eq' instance is lawful.
  = Just $ unsafeCoerce Refl
  | otherwise
  = Nothing

-- | Check whether two GADT values of type @k -> l ->Type@ are equal.
--
-- If so, also return a proof that the tags were equal.
--
-- NB: this is stricter than 'testEquality', as 'testEquality' is supposed
-- to return @Just Refl@ whenever the tags are equal, even when the values
-- themselves are different.
equals2 :: forall a k1 k2 l1 l2.
  ( forall k l. Eq ( a k l)
#if MIN_VERSION_base(4,20,0)
  , forall k l. DataToTag ( a k l )
#endif
  ) => a k1 l1 -> a k2 l2 -> Maybe ( '( k1, l1 ) :~: '( k2, l2 ) )
equals2 k1 k2
  | -- Fail-fast: comparing the tag first.
    isTrue# ( dataToTag# k1 ==# dataToTag# k2 )
    -- Assume the types are the same; this allows us to use the 'Eq' instance.
  , Refl <- ( unsafeCoerce Refl :: '( k1, l1 ) :~: '( k2, l2 ) )
  , k1 == k2
  -- The values are equal (according to the 'Eq' instance): this justifies the
  -- unsafe coercion above, assuming that the 'Eq' instance is lawful.
  = Just $ unsafeCoerce Refl
  | otherwise
  = Nothing

{-------------------------------------------------------------------------------
  Internal auxiliary
-------------------------------------------------------------------------------}

-- | Wrapper that provides a 'GEq' instance definition using 'equals1'
type ApEq :: ( k -> Type ) -> k -> Type
newtype ApEq f a = ApEq ( f a )

instance forall f.
  ( forall tag. Eq ( f tag )
#if MIN_VERSION_base(4,20,0)
  , forall tag. DataToTag (f tag )
#endif
  ) => GEq ( ApEq f ) where
  geq ( ApEq k1 ) ( ApEq k2 ) = equals1 k1 k2