packages feed

stock-0.1.0.0: plugin/Stock/TestEquality.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE BlockArguments #-}
-- | A deliberately /minimal, forward-safe/ 'Data.Type.Equality.TestEquality'
-- (and 'Data.Type.Coercion.TestCoercion') synthesizer.
--
-- It handles exactly the unambiguous "finite singleton" GADT: a one-parameter
-- type whose every constructor is nullary, has no existentials, and pins the
-- parameter to a /ground/ type:
--
-- > data T a where { TInt :: T Int; TBool :: T Bool }
--
-- For these the lawful behaviour is forced: @testEquality x y@ is @Just Refl@
-- exactly when the type /indices/ of @x@ and @y@ coincide (NOT when they are
-- the same constructor: two constructors pinning the same type are equal), and
-- @Nothing@ otherwise.  Because that is the only law-abiding implementation, it
-- can never disagree with a future, more general design, so it commits us to
-- nothing.  Anything outside the subset is refused.
module Stock.TestEquality (synthTestEquality, synthTestCoercion) where

import GHC.Plugins hiding (TcPlugin)
import GHC.Tc.Plugin (TcPluginM, unsafeTcPluginTcM)
import GHC.Tc.Types.Constraint (Ct)
#if MIN_VERSION_ghc(9,12,0)
import GHC.Tc.Types.CtLoc (CtLoc)
#else
import GHC.Tc.Types.Constraint (CtLoc)
#endif
import GHC.Tc.Types.Evidence (EvTerm(EvExpr))
import GHC.Core.Class (Class, classMethods)
import GHC.Core.TyCo.Compare (eqType)
import Stock.Internal

-- | A datacon's GADT equality refinements (no public accessor; via the sig).
dcEqSpec :: DataCon -> [EqSpec]
dcEqSpec dc = let (_, _, eqs, _, _, _) = dataConFullSig dc in eqs

-- | A constructor in the supported subset; returns its pinned ground index.
pinnedGround :: DataCon -> Maybe Type
pinnedGround dc = case dcEqSpec dc of
  [es] | null (dataConExTyCoVars dc)               -- no existentials
       , null (dataConOrigArgTys dc)               -- nullary (no value fields)
       , let ty = snd (eqSpecPair es)
       , isEmptyVarSet (tyCoVarsOfType ty)         -- ground (closed) index
       -> Just ty
  _    -> Nothing

synthTestEquality, synthTestCoercion
  :: GenEnv -> Class -> CtLoc -> Type -> Type -> TcPluginM (Maybe (EvTerm, [Ct]))
synthTestEquality = synthEqLike True
synthTestCoercion = synthEqLike False

-- | @useRefl = True@ ⇒ 'TestEquality' (@(:~:)@ \/ @Refl@); @False@ ⇒
-- 'TestCoercion' (@Coercion@).
synthEqLike :: Bool -> GenEnv -> Class -> CtLoc -> Type -> Type
            -> TcPluginM (Maybe (EvTerm, [Ct]))
synthEqLike useRefl gen cls _loc wrappedTy f =
  case (geStock1 gen, tyConAppTyCon_maybe f) of
    (Just st1Tc, Just fTc)
      | null (tyConAppArgs f)                       -- F is a bare one-param tycon
      , dcons@(dc0 : _) <- tyConDataCons fTc
      , Just pins <- traverse pinnedGround dcons
      , (es0 : _) <- dcEqSpec dc0
      -- the witness type (@(:~:)@ \/ @Coercion@) straight from the method's
      -- signature, so we never have to name a (re-exported) module.
      , (meth : _) <- classMethods cls
      , (witTc : _) <- [ tc | tc <- nonDetEltsUniqSet (tyConsOfType (varType meth))
                            , nameOccName (tyConName tc)
                                == mkTcOcc (if useRefl then ":~:" else "Coercion") ] -> do
          let witCon = tyConSingleDataCon witTc
              kK     = tyVarKind (fst (eqSpecPair es0))
              coAt   = coDown1 gen st1Tc wrappedTy f f
          aTv <- freshTyVarK kK "a"
          bTv <- freshTyVarK kK "b"
          xId <- freshId (mkAppTy wrappedTy (mkTyVarTy aTv)) "x"
          yId <- freshId (mkAppTy wrappedTy (mkTyVarTy bTv)) "y"
          wbX <- freshId (mkTyConApp fTc [mkTyVarTy aTv]) "wx"
          wbY <- freshId (mkTyConApp fTc [mkTyVarTy bTv]) "wy"
          let aTy = mkTyVarTy aTv ; bTy = mkTyVarTy bTv
              witOf x y = mkTyConApp witTc [kK, x, y]
              resTy     = mkTyConApp maybeTyCon [witOf aTy bTy]
              nothingE  = mkCoreConApps nothingDataCon [Type (witOf aTy bTy)]
              -- same index: cox : a~#t, coy : b~#t  ⇒  abCo : a~#b.
              --   Refl     :: forall k (a b). (b ~# a)     => a :~: b    (eqSpec)
              --   Coercion :: forall k (a b). Coercible a b => Coercion a b
              -- so we feed the proof directly; for Coercion we first box the
              -- representational coercion into a Coercible dictionary with the
              -- wired-in 'coercibleDataCon' (@MkCoercible :: (a ~R# b) ->
              -- Coercible a b@).  No Cast, no constraint solving.
              same cox coy =
                let abCo  = mkTransCo (mkCoVarCo cox) (mkSymCo (mkCoVarCo coy))
                    proof | useRefl   = Coercion (mkSymCo abCo)   -- b ~# a (nominal)
                          | otherwise = mkCoreConApps coercibleDataCon
                                          [Type kK, Type aTy, Type bTy
                                          , Coercion (mkSubCo abCo)]   -- a ~R# b boxed
                    wit = mkCoreConApps witCon [Type kK, Type aTy, Type bTy, proof]
                in mkCoreConApps justDataCon [Type (witOf aTy bTy), wit]
          -- testEquality compares the type /indices/, not constructor tags:
          -- two constructors pinning the same ground type ⇒ Just Refl.
          let innerAlts ti cox = mapM mkInner (zip dcons pins)
                where mkInner (dcj, tj) = do
                        coy <- freshCoVar (mkPrimEqPred bTy tj)
                        let rhs = if eqType ti tj then same cox coy else nothingE
                        pure (Alt (DataAlt dcj) [coy] rhs)
          outerAlts <- mapM
            (\(dci, ti) -> do
                cox <- freshCoVar (mkPrimEqPred aTy ti)
                ialts <- innerAlts ti cox
                let inner = Case (Cast (Var yId) (coAt bTy)) wbY resTy ialts
                pure (Alt (DataAlt dci) [cox] inner))
            (zip dcons pins)
          let impl = mkCoreLams [aTv, bTv, xId, yId] $
                       Case (Cast (Var xId) (coAt aTy)) wbX resTy outerAlts
              -- TestEquality/TestCoercion are poly-kinded (@class C (f :: k ->
              -- Type)@), so the dictionary takes the kind @k@ first.
              dict = mkCoreConApps (classDataCon cls) [Type kK, Type wrappedTy, impl]
          pure (Just (EvExpr dict, []))
    _ -> pure Nothing

freshCoVar :: Type -> TcPluginM CoVar
freshCoVar ty = do
  u <- unsafeTcPluginTcM getUniqueM
  pure (mkCoVar (mkSystemName u (mkVarOccFS (fsLit "co"))) ty)