Skip to content

Commit d60a158

Browse files
committed
Don’t recurse forever when inferring roles of recursive types
1 parent 4dae8c8 commit d60a158

2 files changed

Lines changed: 72 additions & 27 deletions

File tree

src/Language/PureScript/TypeChecker/Roles.hs

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ module Language.PureScript.TypeChecker.Roles
1111
import Prelude.Compat
1212

1313
import Data.Coerce (coerce)
14+
import Data.Foldable (fold)
1415
import qualified Data.Map as M
1516
import Data.Maybe (fromMaybe)
17+
import Data.Monoid (Endo(..))
1618
import qualified Data.Set as S
1719
import Data.Text (Text)
1820

@@ -27,7 +29,7 @@ import Language.PureScript.Types
2729
-- ascribed to any given variable, as defined by the @Role@ type's @Ord@
2830
-- instance. That is, a variable that has been marked as @Nominal@ can not
2931
-- later be marked @Representational@, and so on.
30-
newtype RoleMap = RoleMap { getRoleMap :: M.Map Text Role }
32+
newtype RoleMap = RoleMap (M.Map Text Role)
3133

3234
instance Semigroup RoleMap where
3335
(<>) =
@@ -37,46 +39,65 @@ instance Monoid RoleMap where
3739
mempty =
3840
RoleMap M.empty
3941

42+
type RoleEnv = M.Map (Qualified (ProperName 'TypeName)) [Role]
43+
4044
-- |
4145
-- Given an environment and the qualified name of a type constructor in that
4246
-- environment, returns a list of roles, in the order they are defined in the
4347
-- type definition.
4448
inferRoles :: Environment -> Qualified (ProperName 'TypeName) -> [Role]
45-
inferRoles env tyName
46-
| Just roles <- M.lookup tyName (roleDeclarations env) =
47-
roles
48-
| Just (_, DataType tvs ctors) <- envMeta =
49+
inferRoles env tyName =
50+
fold . M.lookup tyName $ updateRoleEnv env tyName (roleDeclarations env)
51+
52+
updateRoleEnv :: Environment -> Qualified (ProperName 'TypeName) -> RoleEnv -> RoleEnv
53+
updateRoleEnv env tyName initialRoleEnv =
54+
case M.lookup tyName (types env) of
55+
Nothing ->
56+
initialRoleEnv
57+
Just ty ->
58+
case updateRoleEnvOnce tyName ty initialRoleEnv of
59+
(updatedRoleEnv, dependencies)
60+
| null dependencies ->
61+
updatedRoleEnv
62+
| otherwise ->
63+
let initialRoleEnv' = foldMap (Endo . updateRoleEnv env) dependencies
64+
`appEndo` updatedRoleEnv
65+
in updateRoleEnv env tyName $ initialRoleEnv'
66+
67+
updateRoleEnvOnce :: Qualified (ProperName 'TypeName) -> (SourceType, TypeKind) -> RoleEnv -> (RoleEnv, S.Set (Qualified (ProperName 'TypeName)))
68+
updateRoleEnvOnce tyName ty roleEnv
69+
| (_, DataType tvs ctors) <- ty =
4970
-- A plain data type. For each constructor the type has, walk its list of
5071
-- field types and accumulate a list of (formal parameter name, role)
5172
-- pairs. Then, walk the list of defined parameters, ensuring both that
5273
-- every parameter appears (with a default role of phantom) and that they
5374
-- appear in the right order.
54-
let ctorRoles = getRoleMap $ foldMap (foldMap (walk mempty) . snd) ctors
55-
in map (\(tv, _) -> fromMaybe Phantom (M.lookup tv ctorRoles)) tvs
56-
| Just (k, ExternData) <- envMeta =
75+
let (RoleMap ctorsRoles, dependencies) = foldMap (foldMap (walk mempty roleEnv) . snd) ctors
76+
roles = map (\(tv, _) -> fromMaybe Phantom (M.lookup tv ctorsRoles)) tvs
77+
in (updateRoles tyName roles roleEnv, dependencies)
78+
| (k, ExternData) <- ty
79+
, Nothing <- M.lookup tyName roleEnv =
5780
-- A foreign data type. Since the type will have no defined constructors
5881
-- nor associated data types, infer the set of type parameters from its
5982
-- kind and assume in the absence of role signatures that all such
6083
-- parameters are nominal.
61-
rolesFromForeignTypeKind k
84+
(updateRoles tyName (rolesFromForeignTypeKind k) roleEnv, mempty)
6285
| otherwise =
63-
[]
86+
(roleEnv, mempty)
6487
where
65-
envTypes = types env
66-
envMeta = M.lookup tyName envTypes
6788
-- This function is named walk to match the specification given in the "Role
6889
-- inference" section of the paper "Safe Zero-cost Coercions for Haskell".
69-
walk :: S.Set Text -> SourceType -> RoleMap
70-
walk btvs (TypeVar _ v)
90+
walk :: S.Set Text -> RoleEnv -> SourceType -> (RoleMap, S.Set (Qualified (ProperName 'TypeName)))
91+
walk btvs _ (TypeVar _ v)
7192
-- A type variable standing alone (e.g. @a@ in @data D a b = D a@) is
7293
-- representational, _unless_ it has been bound by a quantifier, in which
7394
-- case it is not actually a parameter to the type (e.g. @z@ in
7495
-- @data T z = T (forall z. z -> z)@).
7596
| S.member v btvs =
7697
mempty
7798
| otherwise =
78-
RoleMap $ M.singleton v Representational
79-
walk btvs (ForAll _ tv _ t _) =
99+
(RoleMap $ M.singleton v Representational, mempty)
100+
walk btvs roleEnv' (ForAll _ tv _ t _) =
80101
-- We can walk under universal quantifiers as long as we make note of the
81102
-- variables that they bind. For instance, given a definition
82103
-- @data T z = T (forall z. z -> z)@, we will make note that @z@ is bound
@@ -85,15 +106,15 @@ inferRoles env tyName
85106
-- @data D a = D (forall r. r -> a)@, we'll mark @r@ as bound so that it
86107
-- doesn't appear as a spurious parameter to @D@ when we complete
87108
-- inference.
88-
walk (S.insert tv btvs) t
89-
walk btvs (RCons _ _ thead ttail) =
109+
walk (S.insert tv btvs) roleEnv' t
110+
walk btvs roleEnv' (RCons _ _ thead ttail) =
90111
-- For row types, we just walk along them and collect the results.
91-
walk btvs thead <> walk btvs ttail
92-
walk btvs (KindedType _ t _k) =
112+
walk btvs roleEnv' thead <> walk btvs roleEnv' ttail
113+
walk btvs roleEnv' (KindedType _ t _k) =
93114
-- For kind-annotated types, discard the annotation and recurse on the
94115
-- type beneath.
95-
walk btvs t
96-
walk btvs t
116+
walk btvs roleEnv' t
117+
walk btvs roleEnv' t
97118
| (t1, _, t2s) <- unapplyTypes t
98119
, not $ null t2s =
99120
case t1 of
@@ -109,31 +130,37 @@ inferRoles env tyName
109130
-- * If the role is phantom, terminate, since the argument's use of
110131
-- our parameters is unimportant.
111132
TypeConstructor _ t1Name ->
112-
let t1Roles = inferRoles env t1Name
133+
let roles = M.lookup t1Name roleEnv'
134+
inferred = fromMaybe (repeat Phantom) roles
135+
dependencies = maybe (S.singleton t1Name) mempty roles
113136
k role ti = case role of
114137
Nominal ->
115-
freeNominals ti
138+
(freeNominals ti, mempty)
116139
Representational ->
117140
go ti
118141
Phantom ->
119142
mempty
120-
in mconcat (zipWith k t1Roles t2s)
143+
in mappend dependencies <$> mconcat (zipWith k inferred t2s)
121144
-- If the type is an application of any other type-level term, walk
122145
-- that term to collect its roles and mark all free variables in
123146
-- its argument as nominal.
124147
_ ->
125-
go t1 <> foldMap freeNominals t2s
148+
go t1 <> (foldMap freeNominals t2s, mempty)
126149
| otherwise =
127150
mempty
128151
where
129-
go = walk btvs
152+
go = walk btvs roleEnv'
130153
-- Given a type, computes the list of free variables in that type
131154
-- (taking into account those bound in @walk@) and returns a @RoleMap@
132155
-- ascribing a nominal role to each of those variables.
133156
freeNominals x =
134157
let ftvs = filter (flip S.notMember btvs) (freeTypeVariables x)
135158
in RoleMap (M.fromList $ map (, Nominal) ftvs)
136159

160+
updateRoles :: Qualified (ProperName 'TypeName) -> [Role] -> RoleEnv -> RoleEnv
161+
updateRoles = M.insertWith $ \roles' roles ->
162+
uncurry min <$> zip roles' roles
163+
137164
-- |
138165
-- Given the kind of a foreign type, generate a list @Nominal@ roles which, in
139166
-- the absence of a role signature, provides the safest default for a type whose

tests/purs/passing/Coercible.purs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,4 +163,22 @@ type ContextualKeywords =
163163
, role :: String
164164
)
165165

166+
data Recursive a
167+
= RecursiveBaseCase
168+
| Recursive a (Recursive a)
169+
170+
recursiveTerminates :: a. Recursive a
171+
recursiveTerminates = coerce (RecursiveBaseCase :: Recursive (NTLib a))
172+
173+
data MutuallyRecursive1 a
174+
= MutuallyRecursiveBaseCase1
175+
| MutuallyRecursive1 a (MutuallyRecursive2 a)
176+
177+
data MutuallyRecursive2 a
178+
= MutuallyRecursiveBaseCase2
179+
| MutuallyRecursive2 a (MutuallyRecursive1 a)
180+
181+
mutuallyRecursiveTerminates :: a. MutuallyRecursive1 a
182+
mutuallyRecursiveTerminates = coerce (MutuallyRecursiveBaseCase1 :: MutuallyRecursive1 (NTLib a))
183+
166184
main = log (coerce (NTString1 "Done") :: String)

0 commit comments

Comments
 (0)