model: rename `as_notnullable` to `undecorate`
[nit.git] / src / metrics / detect_variance_constraints.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Collect metrics about detected variances constraints on formal types.
16 #
17 # In some OO-language that feature safe invariant generics by default,
18 # formal parameter types can annotated to be usable in covariant and contravariant positions.
19 # The price to pay is that the usage of formal parameter types in the class has constraints:
20 #
21 # * covariant-annotated formal types cannot be used in parameters or in attributes
22 # * contravariant-annotated formal types cannot be used in return or in attributes
23 #
24 # This module provide a backward analysis that infers the possible variance annotations
25 # of formal types in Nit programs by identifying the existing constraints on the usage of those formal type.
26 #
27 # It collects the types used in the signatures of properties then propagates constraints between types.
28 module detect_variance_constraints
29
30 import model
31 import metrics_base
32
33 redef class ToolContext
34 # --detect-variance-constraints
35 var opt_detect_variance_constraints = new OptionBool("Detect the definition-site variance constraints on formal parameters", "--detect-variance-constraints")
36
37 # The DetectVarianceConstraints phase
38 var detect_variance_constraints_phase: Phase = new DetectVarianceConstraintsPhase(self, null)
39 end
40
41 private class DetectVarianceConstraintsPhase
42 super Phase
43
44 init
45 do
46 toolcontext.option_context.add_option(toolcontext.opt_detect_variance_constraints)
47 end
48
49 redef fun process_mainmodule(mainmodule, given_mmodules)
50 do
51 if not toolcontext.opt_detect_variance_constraints.value and not toolcontext.opt_all.value then return
52
53 print "--- Detection of variance constraints on formal parameter types ---"
54
55 var k = new DetectVarianceConstraints
56 k.collect(mainmodule)
57
58 print "-- Generic classes --"
59 k.cpt_class.print_elements(10)
60 print " total classes: {k.cpt_class.sum}"
61 print " total formal parameters: {k.pts.length}"
62
63 k.propagate
64
65 print "-- Including `private` properties --"
66 k.print_stats
67
68 k = new DetectVarianceConstraints
69 k.exclude_private = true
70 k.collect(mainmodule)
71
72 k.propagate
73
74 print "-- Excluding `private` properties --"
75 k.print_stats
76 end
77 end
78
79 # A specific analysis that detects the variance constraints of formal parameters.
80 #
81 # The client has 3 steps to do:
82 #
83 # * call `collect` to initialize the attributes.
84 # * call `propagate` to propagate the variance constraints.
85 # * call `print_stats` to print the results.
86 class DetectVarianceConstraints
87 # Collect all types used in covariant and contravariant positions.
88 #
89 # The collect visits all classes and properties of `mainmodule` and its imported modules.
90 #
91 # After the visit, the attributes of `self` are filled.
92 fun collect(mainmodule: MModule)
93 do
94 for m in mainmodule.in_importation.greaters do
95 for cd in m.mclassdefs do
96 if cd.is_intro then
97 pts.add_all(cd.mclass.mparameters)
98 var a = cd.mclass.arity
99 if a == 0 then
100 cpt_class.inc("non generic")
101 else if a == 1 then
102 cpt_class.inc("with 1 formal type parameter")
103 else
104 cpt_class.inc("with {a} formal type parameters")
105 end
106 end
107 for t in cd.supertypes do
108 # Supertype (covariant)
109 if t.need_anchor then covar_classes.add(t)
110 end
111 for pd in cd.mpropdefs do
112 if exclude_private and pd.mproperty.visibility <= private_visibility then continue
113 if pd isa MMethodDef then
114 # Parameters (contravariant)
115 for p in pd.msignature.mparameters do
116 var t = p.mtype.undecorate
117 if not t.need_anchor then
118 # OK
119 else if t isa MParameterType then
120 contravar_pt.add(t)
121 else if t isa MVirtualType then
122 # TODO?
123 else if t isa MClassType then
124 contravar_classes.add(t)
125 else
126 abort
127 end
128 end
129 # Return (covariant)
130 var t = pd.msignature.return_mtype
131 if t != null and t.need_anchor then
132 t = t.undecorate
133 if t isa MParameterType then
134 covar_pt.add(t)
135 else if t isa MVirtualType then
136 # TODO?
137 else if t isa MClassType then
138 covar_classes.add(t)
139 else
140 abort
141 end
142 end
143 else if pd isa MAttributeDef then
144 # Attribute (invariant)
145 var t = pd.static_mtype
146 if t != null and t.need_anchor then
147 t = t.undecorate
148 if t isa MParameterType then
149 covar_pt.add t
150 contravar_pt.add t
151 else if t isa MVirtualType then
152 # TODO?
153 else if t isa MClassType then
154 covar_classes.add(t)
155 contravar_classes.add(t)
156 else
157 abort
158 end
159 end
160 else if pd isa MVirtualTypeDef then
161 # Virtual type bound (covariant)
162 var t = pd.bound
163 if t != null and t.need_anchor then
164 t = t.undecorate
165 if t isa MParameterType then
166 covar_pt.add t
167 else if t isa MVirtualType then
168 # TODO?
169 else if t isa MClassType then
170 covar_classes.add(t)
171 else
172 abort
173 end
174 end
175 end
176 end
177 end
178 end
179 end
180
181 # The set of all collected formal parameters
182 var pts = new HashSet[MParameterType]
183
184 # The set of generic types found in a covariant (and invariant) position
185 var covar_classes = new HashSet[MClassType]
186
187 # The set of formal parameters found in a covariant (and invariant) position
188 var covar_pt = new HashSet[MParameterType]
189
190 # The set of generic types found in a contravariant (and invariant) position
191 var contravar_classes = new HashSet[MClassType]
192
193 # The set of formal parameters found in a contravariant (and invariant) position
194 var contravar_pt = new HashSet[MParameterType]
195
196 # Classes by number of formal parameters
197 var cpt_class = new Counter[String]
198
199 # Does the collect exclude private properties?
200 # Default is `false`
201 var exclude_private = false
202
203 # Propagate the variance constraints on `covar_classes`, `covar_pt`, `contravar_classes` and `contravar_pt`
204 #
205 # The algorithm uses a fixed-point approach on the covariance/contravariance rules.
206 fun propagate
207 do
208 # Classes to add to the `covar_classes` set at the end of an iteration
209 var new_covar = new Array[MClassType]
210 # Classes to add to the `contravar_classes` set at the end of an iteration
211 var new_contravar = new Array[MClassType]
212 # Does a modification occurred, so that another iteration is needed?
213 var dirty = true
214 # Total number of iterations
215 var cpt = 0
216
217 while dirty do
218 cpt += 1
219 dirty = false
220 new_covar.clear
221 new_contravar.clear
222
223 # Process the generic types in a covariant position
224 for c in covar_classes do for i in [0..c.mclass.arity[ do
225 # The type used in the argument
226 var ta = c.arguments[i].undecorate
227 # The associated formal parameter
228 var tp = c.mclass.mparameters[i]
229
230 if not ta.need_anchor then
231 # Nothing to do
232 else if ta isa MParameterType then
233 # COVAR * COVAR = COVAR
234 if covar_pt.has(tp) and not covar_pt.has(ta) then
235 covar_pt.add(ta)
236 dirty = true
237 end
238 # COVAR * CONTRAVAR = CONTRAVAR
239 if contravar_pt.has(tp) and not contravar_pt.has(ta) then
240 contravar_pt.add(ta)
241 dirty = true
242 end
243 else if ta isa MVirtualType then
244 # TODO?
245 else if ta isa MClassType then
246 # COVAR * COVAR = COVAR
247 if covar_pt.has(tp) and not covar_classes.has(ta) then
248 new_covar.add ta
249 dirty = true
250 end
251 # COVAR * CONTRAVAR = CONTRAVAR
252 if contravar_pt.has(tp) and not contravar_classes.has(ta) then
253 new_contravar.add ta
254 dirty = true
255 end
256 end
257 end
258
259 # Process the generic types in a contravariant position
260 for c in contravar_classes do for i in [0..c.mclass.arity[ do
261 # The type used in the argument
262 var ta = c.arguments[i].undecorate
263 # The associated formal parameter
264 var tp = c.mclass.mparameters[i]
265
266 if not ta.need_anchor then
267 # Nothing to do
268 else if ta isa MParameterType then
269 # CONTRAVAR * CONTRAVAR = COVAR
270 if contravar_pt.has(tp) and not covar_pt.has(ta) then
271 covar_pt.add(ta)
272 dirty = true
273 end
274 # CONTRAVAR * COVAR = CONTRAVAR
275 if covar_pt.has(tp) and not contravar_pt.has(ta) then
276 contravar_pt.add(ta)
277 dirty = true
278 end
279 else if ta isa MVirtualType then
280 # TODO?
281 else if ta isa MClassType then
282 # CONTRAVAR * CONTRAVAR = COVAR
283 if contravar_pt.has(tp) and not covar_classes.has(ta) then
284 new_covar.add ta
285 dirty = true
286 end
287 # CONTRAVAR * COVAR = CONTRAVAR
288 if covar_pt.has(tp) and not contravar_classes.has(ta) then
289 new_contravar.add ta
290 dirty = true
291 end
292 end
293 end
294
295 covar_classes.add_all(new_covar)
296 contravar_classes.add_all(new_contravar)
297 end
298 end
299
300 # Print the final stats on the screen
301 fun print_stats
302 do
303 var nb_cov = 0
304 var nb_con = 0
305 var nb_inv = 0
306 var nb_biv = 0
307
308 for pt in pts do
309 if covar_pt.has(pt) then
310 if contravar_pt.has(pt) then
311 nb_inv += 1
312 else
313 nb_cov += 1
314 #print "covar: {pt.full_name}"
315 end
316 else
317 if contravar_pt.has(pt) then
318 nb_con += 1
319 #print "contravar: {pt.full_name}"
320 else
321 nb_biv += 1
322 #print "bivar: {pt.full_name}"
323 end
324 end
325 end
326
327 print " covariants: {nb_cov} ({div(nb_cov*100, pts.length)}%)"
328 print " contravariants: {nb_con} ({div(nb_con*100, pts.length)}%)"
329 print " bivariants: {nb_biv} ({div(nb_biv*100, pts.length)}%)"
330 print " invariants: {nb_inv} ({div(nb_inv*100, pts.length)}%)"
331 print " total: {pts.length}"
332 end
333 end