purescript-0.15.3: src/Language/PureScript/TypeChecker/Roles.hs
{-# LANGUAGE TypeApplications #-}
-- |
-- Role inference
--
module Language.PureScript.TypeChecker.Roles
( lookupRoles
, checkRoles
, checkRoleDeclarationArity
, inferRoles
, inferDataBindingGroupRoles
) where
import Prelude.Compat
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 qualified Data.Map as M
import Data.Maybe (fromMaybe)
import qualified Data.Set as S
import Data.Semigroup (Any(..))
import Data.Text (Text)
import Language.PureScript.Environment
import Language.PureScript.Errors
import Language.PureScript.Names
import Language.PureScript.Roles
import Language.PureScript.Types
-- |
-- 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)