Skip to content

Consider supporting Typeable instances for data types containing casts

Consider this piece of code, inspired by #15862 (comment 163375):

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
module Bug where

import Data.Kind
import Type.Reflection

type family F a
type instance F Int = Type

data D = forall (a :: F Int). MkD a

tr :: TypeRep (MkD Bool)
tr = typeRep

Currently (on GHC HEAD, which has a fix for #15862 (closed)), this fails with:

[1 of 1] Compiling Bug              ( Bug.hs, Bug.o )

Bug.hs:17:6: error:
    • No instance for (Typeable 'MkD) arising from a use of ‘typeRep’
        GHC can't yet do polykinded Typeable ('MkD :: * -> D)
    • In the expression: typeRep
      In an equation for ‘tr’: tr = typeRep
   |
17 | tr = typeRep
   |      ^^^^^^^

Let's try making a very small change to MkD (note the lack of an explicit a field):

data D' = forall (a :: F Int). MkD'

Accordingly, let's also update tr:

tr :: TypeRep (MkD' @Bool)
tr = typeRep

After these changes, the program now compiles! This is very strange. Why is GHC able to conjure up a Typeable instance for MkD, but not for MkD'?

Some digging revealed the immediate cause of why this is happening. In the bowels of TcTypeable, we only generate Typeable information for type constructors whose kinds don't contain any casts (e.g., the cast implied by the use of F Int), as revealed in this line of code:

typeIsTypeable (CastTy{})           = False

But that clearly isn't the full story, since both MkD and MkD' contain casts. The other piece of the puzzle is this line of code:

tyConIsTypeable tc =
       isJust (tyConRepName_maybe tc)
    && typeIsTypeable (dropForAlls $ tyConKind tc)

The use of dropForAlls means that GHC won't catch the CastTy in the type of MkD', since it's only found in a forall. On the other hand, MkD has a CastTy that's outside of a forall, so GHC does flag that one.


Initially, my reaction was to change GHC to make both MkD and MkD' an error. But then I had a thought: why are CastTys disallowed in typeIsTypeable in the first place? If I compile the version of the program that contains MkD', I get this perfectly sensible-looking Core:

-- RHS size: {terms: 3, types: 4, coercions: 0, joins: 0/0}
$dTypeable_r1Mi :: TypeRep @* Bool
[GblId]
$dTypeable_r1Mi
  = base-4.13.0.0:Data.Typeable.Internal.mkTrCon
      @ * @ Bool GHC.Types.$tcBool (GHC.Types.[] @ SomeTypeRep)

-- RHS size: {terms: 6, types: 12, coercions: 9, joins: 0/0}
$dTypeable1_r1Mj
  :: TypeRep @D' ('MkD' @(Bool |> Sym (Bug.D:R:FInt[0])))
[GblId]
$dTypeable1_r1Mj
  = base-4.13.0.0:Data.Typeable.Internal.mkTrCon
      @ D'
      @ ('MkD' @(Bool |> Sym (Bug.D:R:FInt[0])))
      Bug.$tc'MkD'
      (GHC.Types.:
         @ SomeTypeRep
         (base-4.13.0.0:Data.Typeable.Internal.SomeTypeRep
            @ (F Int)
            @ (Bool |> Sym (Bug.D:R:FInt[0]))
            ($dTypeable_r1Mi
             `cast` ((TypeRep
                        (Sym (Bug.D:R:FInt[0]))
                        (Sym (GRefl nominal (Bool |> Sym (Bug.D:R:FInt[0]))
                                  (Bug.D:R:FInt[0]))))_R
                     :: (TypeRep @* Bool :: *)
                        ~R# (TypeRep @(F Int) (Bool |> Sym (Bug.D:R:FInt[0])) :: *))))
         (GHC.Types.[] @ SomeTypeRep))

-- RHS size: {terms: 2, types: 5, coercions: 7, joins: 0/0}
tr :: TypeRep @D' ('MkD' @(Bool |> Sym (Bug.D:R:FInt[0])))
[GblId]
tr
  = break<0>()
    typeRep
      @ D'
      @ ('MkD' @(Bool |> Sym (Bug.D:R:FInt[0])))
      ($dTypeable1_r1Mj
       `cast` (Sym (base-4.13.0.0:Data.Typeable.Internal.N:Typeable[0]) <D'>_N <'MkD'
                                                                                  @(Bool |> Sym (Bug.D:R:FInt[0]))>_N
               :: (TypeRep @D' ('MkD' @(Bool |> Sym (Bug.D:R:FInt[0]))) :: *)
                  ~R# (Typeable
                         @D' ('MkD' @(Bool |> Sym (Bug.D:R:FInt[0]))) :: Constraint)))

Indeed, this passes -dcore-lint. Every other attempt I've tried at generating Typeable instances for things containing CastTys also works without issue.

In light of this, why should we explicitly disallow CastTys in typeIsTypeable? Why not change this line to be:

typeIsTypeable (CastTy t _) = typeIsTypeable t

Note that this is a separate question from #13933, which is about CoercionTy, not CastTy.

To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information