{-# LANGUAGE TypeApplications #-} -- | -- Role inference -- module Language.PureScript.TypeChecker.Roles ( lookupRoles , checkRoles , checkRoleDeclarationArity , inferRoles , inferDataBindingGroupRoles ) where import Prelude import Control.Arrow ((&&&)) import Control.Monad (unless, when, zipWithM_) import Control.Monad.Error.Class (MonadError(..)) import Control.Monad.State (MonadState(..), runState, state) import Data.Coerce (coerce) import Data.Map qualified as M import Data.Maybe (fromMaybe) import Data.Set qualified as S import Data.Semigroup (Any(..)) import Data.Text (Text) import Language.PureScript.Environment (Environment(..), TypeKind(..)) import Language.PureScript.Errors (DataConstructorDeclaration(..), MultipleErrors, RoleDeclarationData(..), SimpleErrorMessage(..), errorMessage) import Language.PureScript.Names (ModuleName, ProperName, ProperNameType(..), Qualified(..), QualifiedBy(..)) import Language.PureScript.Roles (Role(..)) import Language.PureScript.Types (Constraint(..), SourceType, Type(..), freeTypeVariables, unapplyTypes) -- | -- A map of a type's formal parameter names to their roles. This type's -- @Semigroup@ and @Monoid@ instances preserve the least-permissive role -- ascribed to any given variable, as defined by the @Role@ type's @Ord@ -- instance. That is, a variable that has been marked as @Nominal@ can not -- later be marked @Representational@, and so on. newtype RoleMap = RoleMap { getRoleMap :: M.Map Text Role } instance Semigroup RoleMap where (<>) = coerce @(M.Map Text Role -> _ -> _) @(RoleMap -> _ -> _) (M.unionWith min) instance Monoid RoleMap where mempty = RoleMap M.empty type RoleEnv = M.Map (Qualified (ProperName 'TypeName)) [Role] typeKindRoles :: TypeKind -> Maybe [Role] typeKindRoles = \case DataType _ args _ -> Just $ map (\(_, _, role) -> role) args ExternData roles -> Just roles _ -> Nothing getRoleEnv :: Environment -> RoleEnv getRoleEnv env = M.mapMaybe (typeKindRoles . snd) (types env) updateRoleEnv :: Qualified (ProperName 'TypeName) -> [Role] -> RoleEnv -> (Any, RoleEnv) updateRoleEnv qualTyName roles' roleEnv = let roles = fromMaybe (repeat Phantom) $ M.lookup qualTyName roleEnv mostRestrictiveRoles = zipWith min roles roles' didRolesChange = any (uncurry (<)) $ zip mostRestrictiveRoles roles in (Any didRolesChange, M.insert qualTyName mostRestrictiveRoles roleEnv) -- | -- Lookup the roles for a type in the environment. If the type does not have -- roles (e.g. is a type synonym or a type variable), then this function -- returns an empty list. -- lookupRoles :: Environment -> Qualified (ProperName 'TypeName) -> [Role] lookupRoles env tyName = fromMaybe [] $ M.lookup tyName (types env) >>= typeKindRoles . snd -- | -- Compares the inferred roles to the explicitly declared roles and ensures -- that the explicitly declared roles are not more permissive than the -- inferred ones. -- checkRoles :: forall m . (MonadError MultipleErrors m) => [(Text, Maybe SourceType, Role)] -- ^ type parameters for the data type whose roles we are checking -> [Role] -- ^ roles declared for the data type -> m () checkRoles tyArgs declaredRoles = do let k (var, _, inf) dec = when (inf < dec) . throwError . errorMessage $ RoleMismatch var inf dec zipWithM_ k tyArgs declaredRoles checkRoleDeclarationArity :: forall m . (MonadError MultipleErrors m) => ProperName 'TypeName -> [Role] -> Int -> m () checkRoleDeclarationArity tyName roles expected = do let actual = length roles unless (expected == actual) $ throwError . errorMessage $ RoleDeclarationArityMismatch tyName expected actual -- | -- Infers roles for the given data type declaration. -- inferRoles :: Environment -> ModuleName -> ProperName 'TypeName -- ^ The name of the data type whose roles we are checking -> [(Text, Maybe SourceType)] -- ^ type parameters for the data type whose roles we are checking -> [DataConstructorDeclaration] -- ^ constructors of the data type whose roles we are checking -> [Role] inferRoles env moduleName tyName tyArgs ctors = inferDataBindingGroupRoles env moduleName [] [(tyName, tyArgs, ctors)] tyName tyArgs inferDataBindingGroupRoles :: Environment -> ModuleName -> [RoleDeclarationData] -> [DataDeclaration] -> ProperName 'TypeName -> [(Text, Maybe SourceType)] -> [Role] inferDataBindingGroupRoles env moduleName roleDeclarations group = let declaredRoleEnv = M.fromList $ map (Qualified (ByModuleName moduleName) . rdeclIdent &&& rdeclRoles) roleDeclarations inferredRoleEnv = getRoleEnv env initialRoleEnv = declaredRoleEnv `M.union` inferredRoleEnv inferredRoleEnv' = inferDataBindingGroupRoles' moduleName group initialRoleEnv in \tyName tyArgs -> let qualTyName = Qualified (ByModuleName moduleName) tyName inferredRoles = M.lookup qualTyName inferredRoleEnv' in fromMaybe (Phantom <$ tyArgs) inferredRoles type DataDeclaration = ( ProperName 'TypeName , [(Text, Maybe SourceType)] , [DataConstructorDeclaration] ) inferDataBindingGroupRoles' :: ModuleName -> [DataDeclaration] -> RoleEnv -> RoleEnv inferDataBindingGroupRoles' moduleName group roleEnv = let (Any didRolesChange, roleEnv') = flip runState roleEnv $ mconcat <$> traverse (state . inferDataDeclarationRoles moduleName) group in if didRolesChange then inferDataBindingGroupRoles' moduleName group roleEnv' else roleEnv' -- | -- Infers roles for the given data type declaration, along with a flag to tell -- if more restrictive roles were added to the environment. -- inferDataDeclarationRoles :: ModuleName -> DataDeclaration -> RoleEnv -> (Any, RoleEnv) inferDataDeclarationRoles moduleName (tyName, tyArgs, ctors) roleEnv = let qualTyName = Qualified (ByModuleName moduleName) tyName ctorRoles = getRoleMap . foldMap (walk mempty . snd) $ ctors >>= dataCtorFields inferredRoles = map (\(arg, _) -> fromMaybe Phantom (M.lookup arg ctorRoles)) tyArgs in updateRoleEnv qualTyName inferredRoles roleEnv where -- This function is named @walk@ to match the specification given in the -- "Role inference" section of the paper "Safe Zero-cost Coercions for -- Haskell". walk :: S.Set Text -> SourceType -> RoleMap walk btvs (TypeVar _ v) -- A type variable standing alone (e.g. @a@ in @data D a b = D a@) is -- representational, _unless_ it has been bound by a quantifier, in which -- case it is not actually a parameter to the type (e.g. @z@ in -- @data T z = T (forall z. z -> z)@). | S.member v btvs = mempty | otherwise = RoleMap $ M.singleton v Representational walk btvs (ForAll _ _ tv _ t _) = -- We can walk under universal quantifiers as long as we make note of the -- variables that they bind. For instance, given a definition -- @data T z = T (forall z. z -> z)@, we will make note that @z@ is bound -- by a quantifier so that we do not mark @T@'s parameter as -- representational later on. Similarly, given a definition like -- @data D a = D (forall r. r -> a)@, we'll mark @r@ as bound so that it -- doesn't appear as a spurious parameter to @D@ when we complete -- inference. walk (S.insert tv btvs) t walk btvs (ConstrainedType _ Constraint{..} t) = -- For constrained types, mark all free variables in the constraint -- arguments as nominal and recurse on the type beneath the constraint. walk btvs t <> foldMap (freeNominals btvs) constraintArgs walk btvs (RCons _ _ thead ttail) = do -- For row types, we just walk along them and collect the results. walk btvs thead <> walk btvs ttail walk btvs (KindedType _ t _k) = -- For kind-annotated types, discard the annotation and recurse on the -- type beneath. walk btvs t walk btvs t | (t1, _, t2s) <- unapplyTypes t , not $ null t2s = case t1 of -- If the type is an application of a type constructor to some -- arguments, recursively infer the roles of the type constructor's -- arguments. For each (role, argument) pair: -- -- - If the role is nominal, mark all free variables in the argument -- as nominal also, since they cannot be coerced if the -- argument's nominality is to be preserved. -- -- - If the role is representational, recurse on the argument, since -- its use of our parameters is important. -- -- - If the role is phantom, terminate, since the argument's use of -- our parameters is unimportant. TypeConstructor _ t1Name -> let t1Roles = fromMaybe (repeat Phantom) $ M.lookup t1Name roleEnv k role ti = case role of Nominal -> freeNominals btvs ti Representational -> go ti Phantom -> mempty in mconcat (zipWith k t1Roles t2s) -- If the type is an application of any other type-level term, walk -- that term to collect its roles and mark all free variables in -- its argument as nominal. _ -> do go t1 <> foldMap (freeNominals btvs) t2s | otherwise = mempty where go = walk btvs -- Given a type, computes the list of free variables in that type -- (taking into account those bound in @walk@) and returns a @RoleMap@ -- ascribing a nominal role to each of those variables. freeNominals :: S.Set Text -> SourceType -> RoleMap freeNominals btvs x = let ftvs = filter (flip S.notMember btvs) (freeTypeVariables x) in RoleMap (M.fromList $ map (, Nominal) ftvs)