-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathNormalForm.hs
More file actions
251 lines (212 loc) · 8.96 KB
/
NormalForm.hs
File metadata and controls
251 lines (212 loc) · 8.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
-- | This module defines a normalised representation of APIs, used for
-- comparing them in the migrations changelog, and to analyse dependencies.
module Data.API.NormalForm
( -- * Normalised API types
NormAPI
, NormTypeDecl(..)
, NormRecordType
, NormUnionType
, NormEnumType
-- * Converting to normal form
, apiNormalForm
, declNF
-- * Dependency analysis
, typeDeclsFreeVars
, typeDeclFreeVars
, typeFreeVars
, typeDeclaredInApi
, typeUsedInApi
, typeUsedInTransitiveDep
, transitiveDeps
, transitiveReverseDeps
-- * Invariant validation
, apiInvariant
, declIsValid
, typeIsValid
-- * Modifying types
, substTypeDecl
, substType
, renameTypeUses
) where
import Data.API.PP
import Data.API.Types
import Control.DeepSeq
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Set (Set)
import qualified Data.Set as Set
-- | The API type has too much extra info for us to be able to simply compare
-- them with @(==)@. Our strategy is to strip out ancillary information and
-- normalise into a canonical form, and then we can use a simple @(==)@ compare.
--
-- Our normalised API discards most of the details of each type, keeping
-- just essential information about each type. We discard order of types and
-- fields, so we can use just associative maps.
--
type NormAPI = Map TypeName NormTypeDecl
-- | The normal or canonical form for a type declaration, an 'APINode'.
-- Equality of the normal form indicates equivalence of APIs.
--
-- We track all types.
--
data NormTypeDecl
= NRecordType NormRecordType
| NUnionType NormUnionType
| NEnumType NormEnumType
| NTypeSynonym APIType
| NNewtype BasicType
deriving (Eq, Show)
instance NFData NormTypeDecl where
rnf (NRecordType x) = rnf x
rnf (NUnionType x) = rnf x
rnf (NEnumType x) = rnf x
rnf (NTypeSynonym x) = rnf x
rnf (NNewtype x) = rnf x
-- | The canonical form of a record type is a map from fields to
-- values...
type NormRecordType = Map FieldName APIType
-- | ...similarly a union is a map from fields to alternatives...
type NormUnionType = Map FieldName APIType
-- | ...and an enum is a set of values.
type NormEnumType = Set FieldName
-- | Compute the normal form of an API, discarding extraneous information.
apiNormalForm :: API -> NormAPI
apiNormalForm api =
Map.fromList
[ (name, declNF spec)
| ThNode (APINode {anName = name, anSpec = spec}) <- api ]
-- | Compute the normal form of a single type declaration.
declNF :: Spec -> NormTypeDecl
declNF (SpRecord (SpecRecord fields)) = NRecordType $ Map.fromList
[ (fname, ftType ftype)
| (fname, ftype) <- fields ]
declNF (SpUnion (SpecUnion alts)) = NUnionType $ Map.fromList
[ (fname, ftype)
| (fname, (ftype, _)) <- alts ]
declNF (SpEnum (SpecEnum elems)) = NEnumType $ Set.fromList
[ fname | (fname, _) <- elems ]
declNF (SpSynonym t) = NTypeSynonym t
declNF (SpNewtype (SpecNewtype bt _)) = NNewtype bt
-------------------------
-- Dependency analysis
--
-- | Find the set of type names used in an API
typeDeclsFreeVars :: NormAPI -> Set TypeName
typeDeclsFreeVars = Set.unions . map typeDeclFreeVars . Map.elems
-- | Find the set of type names used in a declaration
typeDeclFreeVars :: NormTypeDecl -> Set TypeName
typeDeclFreeVars (NRecordType fields) = Set.unions . map typeFreeVars
. Map.elems $ fields
typeDeclFreeVars (NUnionType alts) = Set.unions . map typeFreeVars
. Map.elems $ alts
typeDeclFreeVars (NEnumType _) = Set.empty
typeDeclFreeVars (NTypeSynonym t) = typeFreeVars t
typeDeclFreeVars (NNewtype _) = Set.empty
-- | Find the set of type names used in an type
typeFreeVars :: APIType -> Set TypeName
typeFreeVars (TyList t) = typeFreeVars t
typeFreeVars (TySet t) = typeFreeVars t
typeFreeVars (TyMaybe t) = typeFreeVars t
typeFreeVars (TyName n) = Set.singleton n
typeFreeVars (TyBasic _) = Set.empty
typeFreeVars TyJSON = Set.empty
-- | Check if a type is declared in the API
typeDeclaredInApi :: TypeName -> NormAPI -> Bool
typeDeclaredInApi tname api = Map.member tname api
-- | Check if a type is used anywhere in the API
typeUsedInApi :: TypeName -> NormAPI -> Bool
typeUsedInApi tname api = tname `Set.member` typeDeclsFreeVars api
-- | Check if the first type's transitive dependencies include the
-- second type
typeUsedInTransitiveDep :: TypeName -> TypeName -> NormAPI -> Bool
typeUsedInTransitiveDep root tname api =
tname == root || tname `Set.member` transitiveDeps api (Set.singleton root)
-- | Compute the transitive dependencies of a set of types
transitiveDeps :: NormAPI -> Set TypeName -> Set TypeName
transitiveDeps api = transitiveClosure $ \ s ->
typeDeclsFreeVars $
Map.filterWithKey (\ x _ -> x `Set.member` s) api
-- | Compute the set of types that depend (transitively) on the given types
transitiveReverseDeps :: NormAPI -> Set TypeName -> Set TypeName
transitiveReverseDeps api = transitiveClosure $ \ s ->
Map.keysSet $
Map.filter (intersects s . typeDeclFreeVars) api
where
intersects s1 s2 = not $ Set.null $ s1 `Set.intersection` s2
-- | Compute the transitive closure of a relation. Relations are
-- represented as functions that takes a set of elements to the set of
-- related elements.
transitiveClosure :: Ord a => (Set a -> Set a) -> Set a -> Set a
transitiveClosure rel x = findUsed x0 x0
where
x0 = rel x
findUsed seen old
| Set.null new = seen
| otherwise = findUsed (seen `Set.union` new) new
where
new = rel old `Set.difference` seen
-------------------------
-- Invariant validation
--
-- | Test that all the free type names in a type are declared in the
-- API. If not, return the set of undeclared types.
typeIsValid :: APIType -> NormAPI -> Either (Set TypeName) ()
typeIsValid t api
| typeVars `Set.isSubsetOf` declaredTypes = return ()
| otherwise = Left (typeVars Set.\\ declaredTypes)
where
typeVars = typeFreeVars t
declaredTypes = Map.keysSet api
-- | Test that all the types used in a type declaration are declared
-- in the API. If not, return the set of undeclared types.
declIsValid :: NormTypeDecl -> NormAPI -> Either (Set TypeName) ()
declIsValid decl api
| declVars `Set.isSubsetOf` declaredTypes = return ()
| otherwise = Left (declVars Set.\\ declaredTypes)
where
declVars = typeDeclFreeVars decl
declaredTypes = Map.keysSet api
-- | Test that all the types used in the API are declared. If not,
-- return the set of undeclared types.
apiInvariant :: NormAPI -> Either (Set TypeName) ()
apiInvariant api
| usedTypes `Set.isSubsetOf` declaredTypes = return ()
| otherwise = Left (usedTypes Set.\\ declaredTypes)
where
usedTypes = typeDeclsFreeVars api
declaredTypes = Map.keysSet api
-------------------------
-- Modifying types
--
-- | Substitute types for type names in a declaration
substTypeDecl :: (TypeName -> APIType) -> NormTypeDecl -> NormTypeDecl
substTypeDecl f (NRecordType fields) = NRecordType (Map.map (substType f) fields)
substTypeDecl f (NUnionType alts) = NUnionType (Map.map (substType f) alts)
substTypeDecl _ d@(NEnumType _) = d
substTypeDecl f (NTypeSynonym t) = NTypeSynonym (substType f t)
substTypeDecl _ d@(NNewtype _) = d
-- | Substitute types for type names in a type
substType :: (TypeName -> APIType) -> APIType -> APIType
substType f (TyList t) = TyList (substType f t)
substType f (TySet t) = TySet (substType f t)
substType f (TyMaybe t) = TyMaybe (substType f t)
substType f (TyName n) = f n
substType _ t@(TyBasic _) = t
substType _ t@TyJSON = t
-- | Rename the first type to the second throughout the API
renameTypeUses :: TypeName -> TypeName -> NormAPI -> NormAPI
renameTypeUses tname tname' = Map.map (substTypeDecl rename)
where
rename tn | tn == tname = TyName tname'
| otherwise = TyName tn
instance PPLines NormTypeDecl where
ppLines (NRecordType flds) = "record" : map (\ (f, ty) -> " " ++ pp f
++ " :: " ++ pp ty)
(Map.toList flds)
ppLines (NUnionType alts) = "union" : map (\ (f, ty) -> " | " ++ pp f
++ " :: " ++ pp ty)
(Map.toList alts)
ppLines (NEnumType vals) = "enum" : map (\ v -> " | " ++ pp v)
(Set.toList vals)
ppLines (NTypeSynonym t) = [pp t]
ppLines (NNewtype b) = ["basic " ++ pp b]