Rewriting type families with GHC type-checking plugins

HIW 2021

Sam Derbyshire, Tweag

August 22nd, 2021

Introduction

Talk overview


  • When to reach for a type-checking plugin?
  • Lightning fast review of type-checker plugins.
  • New functionality: rewriting type family applications.
  • Illustration: intrinsically typed System F.
  • Presentation of a library for writing type-checking plugins.

When to reach for a type-checking plugin?

Equational theory: Peano arithmetic


GHC doesn’t understand basic arithmetic.


type (+) :: Nat -> Nat -> Nat
type family a + b where
  Zero   + b = b
  Succ a + b = Succ (a + b)

GHC can only reduce a + b when it knows what a is:
is it Zero or is it Succ i for some i?


Can’t reason about (+) parametrically:

Could not deduce n + Succ n ~ Succ (n + n)

Equational theory: row types


Row: unordered association map, field name ⇝ type, e.g.
( "field1" :: Int, "field2" :: Bool ).
Can be used to model extensible records (order doesn’t matter).


To account for re-ordering, we need:

Insert k v (Insert l w r) ~ Insert l w (Insert k v r)

when k and l are distinct field names that don’t appear in the row r.

Problem: in GHC, we can’t have type-families on the LHS of a type family equation:

• Illegal type synonym family application ‘Insert l w r’ in instance:
    Insert k v (Insert l w r)

Non-confluence


GHC can’t allow type-families on the LHS of a type family equation in general: no guarantee type-family reduction is confluent.


type F :: Type -> Type
type family F a where
  F (F a) = a       -- F[0]
  F Bool  = Float   -- F[1]
  F a     = Maybe a -- F[2]

F (F Bool) ~F[0]~> Bool
F (F Bool) ~(F F[1])~> F Float ~F[2]~> Maybe Float

Type-checking plugins to the rescue

Rewriter plugins


data TcPlugin = forall s. TcPlugin
  { tcPluginInit    :: TcPluginM s
  , tcPluginSolve   :: s -> TcPluginSolver
  , tcPluginRewrite :: s -> UniqFM TyCon TcPluginRewriter -- new!
  , tcPluginStop    :: s -> TcPluginM ()
  }

type TcPluginRewriter
  =  [Ct]   -- Givens
  -> [Type] -- Type family arguments
  -> TcPluginM TcPluginRewriteResult

data TcPluginRewriteResult
  = TcPluginNoRewrite
  | TcPluginRewriteTo
    { tcPluginReduction    :: !Reduction
    , tcRewriterNewWanteds :: [Ct]
    }

data Reduction = Reduction Coercion !Type

Stephanie Weirich’s challenge


Stephanie Weirich posed the challenge of implementing a strongly-typed representation of System F. [Talk] [GitHub]


-- | A term, in the given context, of the given type.
data Term ϕ a where
  -- | Function application.
  (:$) :: Term ϕ (a :-> b) -> Term ϕ a -> Term ϕ b
  -- | Type application.
  (:@) :: Term ϕ (Forall ty) -> Sing a -> Term ϕ (SubOne a ty)
  -- Elided: variables, literals, Lambda abstraction, Big Lambda.

Type-level substitution


type ApplySub ::
    forall (kϕ :: KContext) (kψ :: KContext) (k :: Kind).
    Sub kϕ kψ -> Type kϕ k -> Type kψ k
-- | Apply a substitution to a type.
type family ApplySub s ty where {..}

-- | Substitute a single type.
type SubOne a b = ApplySub ('Extend 'Id a) b

Problem


ApplySub s (ApplySub t a) ~ ApplySub ('Compose s t) a

Type family argument on the LHS of a type family equation.


Might also need to use Givens:

[G] ApplySub t a ~ f

ApplySub s f
  ~~>
  ApplySub ('Compose s t) a

Rewriting plugin


pluginRewrite :: PluginDefs -> UniqFM TyCon TcPluginRewriter
pluginRewrite defs@( PluginDefs { applySubTyCon } ) =
  listToUFM
    [ ( applySubTyCon, rewriteApplySub defs ) ]

rewriteApplySub
  :: PluginDefs
  -> [ Ct ]     -- Givens
  -> [ Type ]   -- Arguments to ApplySub
  -> TcPluginM TcPluginRewriteResult
rewriteApplySub defs givens [ kϕ, kψ, k, sub, sub_arg ]
  = ...


rewriteApplySub defs givens [ kϕ, kψ, k, sub, sub_arg ]
  = ...

[G] ApplySub kϕ0 kϕ l t a ~ sub_arg

rewriteApplySub defs givens [ kϕ, kψ, k, sub, sub_arg ]
  ~~>
  TcPluginRewriteTo
    { tcPluginReduction =
        mkPluginNomRedn "ApplySub s (ApplySub t a)" $
          mkTyConApp composeTyCon [kϕ0, kϕ, kψ, sub, t]
    , tcRewriterNewWanteds = [] }

Type-checking plugins in practice?

import TcPluginM (TcPluginM, tcPluginTrace, tcPluginIO)import Type (Kind, PredType, eqType, mkTyVarTy, tyConAppTyCon_maybe)import TysWiredIn (typeNatKind)import Coercion (CoercionHole, Role (..), mkUnivCo)import Class (className)import TcPluginM (newCoercionHole, tcLookupClass, newEvVar)import TcRnTypes (TcPlugin (..), TcPluginResult(..))import TyCoRep (UnivCoProvenance (..))import TcType (isEqPred)import TyCon (TyCon)import TyCoRep (Type (..))import TcTypeNats (typeNatAddTyCon, typeNatExpTyCon, typeNatMulTyCon, typeNatSubTyCon)import TcTypeNats (typeNatLeqTyCon)import TysWiredIn (promotedFalseDataCon, promotedTrueDataCon)#if MIN_VERSION_ghc(8,10,0)import Constraint (Ct, CtEvidence (..), CtLoc, TcEvDest (..), ctEvidence, ctEvLoc, ctEvPred, ctLoc, ctLocSpan, isGiven, isWanted, mkNonCanonical, setCtLoc, setCtLocSpan, isWantedCt)import Predicate (EqRel (NomEq), Pred (EqPred), classifyPredType, getEqPredTys, mkClassPred, mkPrimEqPred, getClassPredTys_maybe)import Type (typeKind)#elseimport TcRnTypes (Ct, CtEvidence (..), CtLoc, TcEvDest (..), ctEvidence, ctEvLoc, ctEvPred, ctLoc, ctLocSpan, isGiven, isWanted, mkNonCanonical, setCtLoc, setCtLocSpan, isWantedCt)import TcType (typeKind)import Type (EqRel (NomEq), PredTree (EqPred), classifyPredType, mkClassPred, mkPrimEqPred, getClassPredTys_maybe)#if MIN_VERSION_ghc(8,4,0)import Type (getEqPredTys)#endif#endif#if MIN_VERSION_ghc(8,10,0)import Constraint (ctEvExpr)#elif MIN_VERSION_ghc(8,6,0)import TcRnTypes (ctEvExpr)#elseimport TcRnTypes (ctEvTerm)#endif#if MIN_VERSION_ghc(8,2,0)#if MIN_VERSION_ghc(8,10,0)import Constraint (ShadowInfo (WDeriv))#elseimport TcRnTypes (ShadowInfo (WDeriv))#endif#endif#if MIN_VERSION_ghc(8,10,0)import TcType (isEqPrimPred)#endif#endif– GHC API#if MIN_VERSION_ghc(9,0,0)import GHC.Builtin.Names (knownNatClassName, eqTyConKey, heqTyConKey, hasKey)import GHC.Builtin.Types (promotedFalseDataCon, promotedTrueDataCon)import GHC.Builtin.Types.Literals (typeNatAddTyCon, typeNatExpTyCon, typeNatMulTyCon, typeNatSubTyCon)#if MIN_VERSION_ghc(9,2,0)import GHC.Builtin.Types (naturalTy)import GHC.Builtin.Types.Literals (typeNatCmpTyCon)#elseimport GHC.Builtin.Types (typeNatKind)import GHC.Builtin.Types.Literals (typeNatLeqTyCon)#endifimport GHC.Core (Expr (..))import GHC.Core.Class (className)import GHC.Core.Coercion (CoercionHole, Role (..), mkUnivCo)import GHC.Core.Predicate (EqRel (NomEq), Pred (EqPred), classifyPredType, getEqPredTys, mkClassPred, mkPrimEqPred, isEqPred, isEqPrimPred, getClassPredTys_maybe)import GHC.Core.TyCo.Rep (Type (..), UnivCoProvenance (..))import GHC.Core.TyCon (TyCon)import GHC.Core.Type (Kind, PredType, eqType, mkTyVarTy, tyConAppTyCon_maybe, typeKind)import GHC.Driver.Plugins (Plugin (..), defaultPlugin, purePlugin)import GHC.Tc.Plugin (TcPluginM, newCoercionHole, tcLookupClass, tcPluginTrace, tcPluginIO, newEvVar)#if MIN_VERSION_ghc(9,2,0)import GHC.Tc.Plugin (tcLookupTyCon)#endifimport GHC.Tc.Types (TcPlugin (..), TcPluginResult (..))import GHC.Tc.Types.Constraint (Ct, CtEvidence (..), CtLoc, TcEvDest (..), ShadowInfo (WDeriv), ctEvidence, ctLoc, ctLocSpan, isGiven, isWanted, mkNonCanonical, setCtLoc, setCtLocSpan, isWantedCt, ctEvLoc, ctEvPred, ctEvExpr)import GHC.Tc.Types.Evidence (EvTerm (..), evCast, evId)#if MIN_VERSION_ghc(9,2,0)import GHC.Data.FastString (fsLit)import GHC.Types.Name.Occurrence (mkTcOcc)import GHC.Unit.Module (mkModuleName)#endifimport GHC.Utils.Outputable (Outputable (..), (<+>), (<>), text)#else#if MIN_VERSION_ghc(8,5,0)import CoreSyn (Expr (..))#endifimport Outputable (Outputable (..), (<+>), (<>), text)import Plugins (Plugin (..), defaultPlugin)#if MIN_VERSION_ghc(8,6,0)import Plugins (purePlugin)#endifimport PrelNames (hasKey, knownNatClassName)import PrelNames (eqTyConKey, heqTyConKey)import TcEvidence (EvTerm (..))#if MIN_VERSION_ghc(8,6,0)import TcEvidence (evCast, evId)#endif#if !MIN_VERSION_ghc(8,4,0)import TcPluginM (zonkCt)#endifimport GHC.TcPlugin.API

Typed holes


import GHC.TcPlugin.API

-- Want to use a 'Coercion' as equality constraint evidence.
help :: Coercion -> EvTerm
help = _

    * Found hole: _ :: Coercion -> EvTerm
    * Valid hole fits include
        evCoercion :: Coercion -> EvTerm

Typed holes (2)


import GHC.TcPlugin.API

-- Want to apply a 'TyCon' to arguments.
help :: TyCon -> [Type] -> Type
help = _

    * Found hole: _ :: TyCon -> [Type] -> Type
    * Valid hole fits include
        mkTyConApp :: TyCon -> [Type] -> Type

Hackage documentation

Custom type errors


Helpers for custom type errors.


myErrMsg :: Type -> TcPluginErrorMessage
myErrMsg ty =
   Txt "Couldn't handle type " :|: PrintType ty :|: Txt "."
  -- Same process as with the 'ErrorMessage' data kind.

errorWantedCt :: MonadTcPluginWork m => CtLoc -> Type -> m Ct
errorWantedCt loc ty =
   newWanted loc $ mkTcPluginErrorTy (myErrMsg ty)

Available now!


Get your hands dirty: ghc-tcplugin-api on Hackage.


Type-family rewriting functionality in GHC 9.4.
Compatibility layer in ghc-tcplugin-api for GHC 9.0 and 9.2.


ghc-tcplugin-api on GitHub contains the System F example.
Slides: github.com/sheaf/HIW-tcplugins


Upcoming Tweag blog post: writing and debugging a type-checking plugin.
In depth review of GHC constraint solving and type family reduction.