-
Notifications
You must be signed in to change notification settings - Fork 571
Check roles of mutually recursive types #3860
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,27 +5,27 @@ | |
| -- Role inference | ||
| -- | ||
| module Language.PureScript.TypeChecker.Roles | ||
| ( lookupEnvRoles | ||
| ( lookupRoles | ||
| , checkRoles | ||
| , checkDataBindingGroupRoles | ||
| ) where | ||
|
|
||
| import Prelude.Compat | ||
|
|
||
| import Control.Monad | ||
| import Control.Monad.Error.Class (MonadError(..)) | ||
| import Control.Monad.State.Class (MonadState(..)) | ||
| 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 | ||
| import Language.PureScript.TypeChecker.Monad | ||
|
|
||
| -- | | ||
| -- A map of a type's formal parameter names to their roles. This type's | ||
|
|
@@ -43,23 +43,43 @@ 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. | ||
| -- | ||
| lookupEnvRoles | ||
| lookupRoles | ||
| :: Environment | ||
| -> Qualified (ProperName 'TypeName) | ||
| -> [Role] | ||
| lookupEnvRoles env tyName = | ||
| case fmap snd $ M.lookup tyName (types env) of | ||
| Just (DataType args _) -> | ||
| map (\(_, _, role) -> role) args | ||
| Just (ExternData roles) -> | ||
| roles | ||
| _ -> | ||
| [] | ||
| lookupRoles env tyName = | ||
| fromMaybe [] $ M.lookup tyName (types env) >>= typeKindRoles . snd | ||
|
|
||
| -- | This function does the following: | ||
| -- | ||
|
|
@@ -71,47 +91,92 @@ lookupEnvRoles env tyName = | |
| -- | ||
| checkRoles | ||
| :: forall m | ||
| . (MonadError MultipleErrors m, MonadState CheckState m) | ||
| => ModuleName | ||
| . (MonadError MultipleErrors m) | ||
| => 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 | ||
| -> m [Role] | ||
| checkRoles moduleName tyName tyArgs ctors = do | ||
| let qualName = Qualified (Just moduleName) tyName | ||
| roleMaps <- traverse (walk mempty . snd) $ ctors >>= dataCtorFields | ||
| let | ||
| ctorRoles = getRoleMap $ mconcat roleMaps | ||
| inferredRoles = map (\(arg, _) -> fromMaybe Phantom (M.lookup arg ctorRoles)) tyArgs | ||
| env <- getEnv | ||
| rethrow (addHint (ErrorInRoleDeclaration tyName)) $ do | ||
| case M.lookup qualName (roleDeclarations env) of | ||
| Just declaredRoles -> do | ||
| let | ||
| k (var, _) inf dec = | ||
| if inf < dec | ||
| then throwError . errorMessage $ RoleMismatch var inf dec | ||
| else pure dec | ||
| sequence $ zipWith3 k tyArgs inferredRoles declaredRoles | ||
| Nothing -> | ||
| pure inferredRoles | ||
| checkRoles env moduleName tyName tyArgs ctors = | ||
| checkDataBindingGroupRoles env moduleName [(tyName, tyArgs, ctors)] tyName tyArgs | ||
|
|
||
| type DataDeclaration = | ||
| ( ProperName 'TypeName | ||
| , [(Text, Maybe SourceType)] | ||
| , [DataConstructorDeclaration] | ||
| ) | ||
|
|
||
| checkDataBindingGroupRoles | ||
| :: forall m | ||
| . (MonadError MultipleErrors m) | ||
| => Environment | ||
| -> ModuleName | ||
| -> [DataDeclaration] | ||
| -> ProperName 'TypeName | ||
| -> [(Text, Maybe SourceType)] | ||
| -> m [Role] | ||
| checkDataBindingGroupRoles env moduleName group = | ||
| let initialRoleEnv = M.union (roleDeclarations env) (getRoleEnv env) | ||
| inferredRoleEnv = inferDataBindingGroupRoles moduleName group initialRoleEnv | ||
| in \tyName tyArgs -> do | ||
| let qualTyName = Qualified (Just moduleName) tyName | ||
| inferredRoles = M.lookup qualTyName inferredRoleEnv | ||
| rethrow (addHint (ErrorInRoleDeclaration tyName)) $ do | ||
| case M.lookup qualTyName (roleDeclarations env) of | ||
| Just declaredRoles -> do | ||
| let | ||
| k (var, _) inf dec = | ||
| if inf < dec | ||
| then throwError . errorMessage $ RoleMismatch var inf dec | ||
| else pure dec | ||
| sequence $ zipWith3 k tyArgs (fromMaybe (repeat Phantom) inferredRoles) declaredRoles | ||
| Nothing -> | ||
| pure $ fromMaybe (Phantom <$ tyArgs) inferredRoles | ||
|
|
||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you add some documentation about the meaning of the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added a comment and aliased the |
||
| :: ModuleName | ||
| -> DataDeclaration | ||
| -> RoleEnv | ||
| -> (Any, RoleEnv) | ||
| inferDataDeclarationRoles moduleName (tyName, tyArgs, ctors) roleEnv = | ||
| let qualTyName = Qualified (Just 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 -> m RoleMap | ||
| 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 = | ||
| pure mempty | ||
| mempty | ||
| | otherwise = | ||
| pure $ RoleMap $ M.singleton v Representational | ||
| 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 | ||
|
|
@@ -124,9 +189,7 @@ checkRoles moduleName tyName tyArgs ctors = do | |
| walk (S.insert tv btvs) t | ||
| walk btvs (RCons _ _ thead ttail) = do | ||
| -- For row types, we just walk along them and collect the results. | ||
| h <- walk btvs thead | ||
| t <- walk btvs ttail | ||
| pure (h <> t) | ||
| walk btvs thead <> walk btvs ttail | ||
| walk btvs (KindedType _ t _k) = | ||
| -- For kind-annotated types, discard the annotation and recurse on the | ||
| -- type beneath. | ||
|
|
@@ -146,26 +209,24 @@ checkRoles moduleName tyName tyArgs ctors = do | |
| -- 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 -> do | ||
| env <- getEnv | ||
| TypeConstructor _ t1Name -> | ||
| let | ||
| t1Roles = lookupEnvRoles env t1Name | ||
| t1Roles = fromMaybe (repeat Phantom) $ M.lookup t1Name roleEnv | ||
| k role ti = case role of | ||
| Nominal -> | ||
| pure $ freeNominals ti | ||
| freeNominals ti | ||
| Representational -> | ||
| go ti | ||
| Phantom -> | ||
| pure mempty | ||
| fmap mconcat (zipWithM k t1Roles t2s) | ||
| 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 | ||
| r <- go t1 | ||
| pure (r <> foldMap freeNominals t2s) | ||
| go t1 <> foldMap freeNominals t2s | ||
| | otherwise = | ||
| pure mempty | ||
| mempty | ||
| where | ||
| go = walk btvs | ||
| -- Given a type, computes the list of free variables in that type | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| Error found: | ||
| in module [33mMain[0m | ||
| at tests/purs/failing/CoercibleRoleMismatch4.purs:4:1 - 4:19 (line 4, column 1 - line 4, column 19) | ||
|
|
||
| Role mismatch for the type parameter [33ma[0m: | ||
|
|
||
| The annotation says [33mrepresentational[0m but the role [33mnominal[0m is required. | ||
|
|
||
|
|
||
| in role declaration for [33mF[0m | ||
| in data binding group F, G | ||
|
|
||
| See https://github.com/purescript/documentation/blob/master/errors/RoleMismatch.md for more information, | ||
| or to contribute content related to this error. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| -- @shouldFailWith RoleMismatch | ||
| module Main where | ||
|
|
||
| data F a = F (G a) | ||
| type role F representational | ||
|
|
||
| data G a = G (F a) | ||
| type role G nominal |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| Error found: | ||
| in module [33mMain[0m | ||
| at tests/purs/failing/CoercibleRoleMismatch5.purs:4:1 - 4:21 (line 4, column 1 - line 4, column 21) | ||
|
|
||
| Role mismatch for the type parameter [33ma[0m: | ||
|
|
||
| The annotation says [33mphantom[0m but the role [33mrepresentational[0m is required. | ||
|
|
||
|
|
||
| in role declaration for [33mF[0m | ||
| in data binding group F, G | ||
|
|
||
| See https://github.com/purescript/documentation/blob/master/errors/RoleMismatch.md for more information, | ||
| or to contribute content related to this error. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| -- @shouldFailWith RoleMismatch | ||
| module Main where | ||
|
|
||
| data F a = F a (G a) | ||
| type role F phantom | ||
|
|
||
| data G a = G (F a) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than keeping this function "pure", I think it might be preferable (or at least better aligned with the way the rest of the type checker is written) for it to operate in
MonadState CheckState m, and read and write directly from the actual type-checking environment, rather than doing its own thing at first and then later updating the environment after inferring/checking roles. I don't feel particularly strongly about this, though. @natefaubion do you have any thoughts on this?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really have much of an opinion. Is there some currying advantage to how the functions are arranged, and how it returns an additional lambda, that's exploited elsewhere?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The explicit floating of
initialRoleEnvandinferredRoleEnvoutside of the do block incheckDataBindingGroupRolesis supposed to allow the roles of the data declarations in the binding group to be inferred only once and then looked up repeatedly in theDataBindingGroupDeclarationcase ofLanguage.PureScript.TypeChecker.typeCheckAll. This isn’t something I’ve verified because I’m not familiar with Core inspection but I’m not sure GHC would be able to share the work otherwise.I can try to store the inferred roles environment in the typechecker monad state and separate roles inference from roles checking in binding groups. This should be moree performant than computing a new roles map from scratch each time we need to check roles.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since neither @natefaubion nor I feel strongly I’m tempted to say let’s leave this as is for now. I’m happy to merge once there’s a comment about what the Any in inferDataDeclarationRoles means.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I guess it’s that we want to keep walking over all of the data declarations in the binding group until we reach a point where walking doesn’t change anything in the roleEnv, and the Any part of the result indicates whether something changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I lifted the role inference to the typechecker monad!