src: move collections from model to a new module
[nit.git] / src / modelbuilder.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2012 Jean Privat <jean@pryen.org>
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Load nit source files and build the associated model
18 #
19 # FIXME better doc
20 #
21 # FIXME split this module into submodules
22 # FIXME add missing error checks
23 module modelbuilder
24
25 import parser
26 import model
27 import poset
28 import opts
29 import toolcontext
30 import phase
31
32 private import more_collections
33
34 ###
35
36 redef class ToolContext
37 # Option --path
38 readable var _opt_path: OptionArray = new OptionArray("Set include path for loaders (may be used more than once)", "-I", "--path")
39
40 # Option --only-metamodel
41 readable var _opt_only_metamodel: OptionBool = new OptionBool("Stop after meta-model processing", "--only-metamodel")
42
43 # Option --only-parse
44 readable var _opt_only_parse: OptionBool = new OptionBool("Only proceed to parse step of loaders", "--only-parse")
45
46 redef init
47 do
48 super
49 option_context.add_option(opt_path, opt_only_parse, opt_only_metamodel)
50 end
51
52 fun modelbuilder: ModelBuilder do return modelbuilder_real.as(not null)
53 private var modelbuilder_real: nullable ModelBuilder = null
54
55 end
56
57 # A model builder knows how to load nit source files and build the associated model
58 class ModelBuilder
59 # The model where new modules, classes and properties are added
60 var model: Model
61
62 # The toolcontext used to control the interaction with the user (getting options and displaying messages)
63 var toolcontext: ToolContext
64
65 # Run phases on all loaded modules
66 fun run_phases
67 do
68 var mmodules = model.mmodules.to_a
69 model.mmodule_importation_hierarchy.sort(mmodules)
70 var nmodules = new Array[AModule]
71 for mm in mmodules do
72 nmodules.add(mmodule2nmodule[mm])
73 end
74 toolcontext.run_phases(nmodules)
75 end
76
77 # Instantiate a modelbuilder for a model and a toolcontext
78 # Important, the options of the toolcontext must be correctly set (parse_option already called)
79 init(model: Model, toolcontext: ToolContext)
80 do
81 self.model = model
82 self.toolcontext = toolcontext
83 assert toolcontext.modelbuilder_real == null
84 toolcontext.modelbuilder_real = self
85
86 # Setup the paths value
87 paths.append(toolcontext.opt_path.value)
88
89 var path_env = "NIT_PATH".environ
90 if not path_env.is_empty then
91 paths.append(path_env.split_with(':'))
92 end
93
94 path_env = "NIT_DIR".environ
95 if not path_env.is_empty then
96 var libname = "{path_env}/lib"
97 if libname.file_exists then paths.add(libname)
98 end
99
100 var libname = "{sys.program_name.dirname}/../lib"
101 if libname.file_exists then paths.add(libname.simplify_path)
102 end
103
104 # Load a bunch of modules.
105 # `modules' can contains filenames or module names.
106 # Imported modules are automatically loaded and modelized.
107 # The result is the corresponding model elements.
108 # Errors and warnings are printed with the toolcontext.
109 #
110 # Note: class and property model element are not analysed.
111 fun parse(modules: Sequence[String]): Array[MModule]
112 do
113 var time0 = get_time
114 # Parse and recursively load
115 self.toolcontext.info("*** PARSE ***", 1)
116 var mmodules = new Array[MModule]
117 for a in modules do
118 var nmodule = self.load_module(null, a)
119 if nmodule == null then continue # Skip error
120 mmodules.add(nmodule.mmodule.as(not null))
121 end
122 var time1 = get_time
123 self.toolcontext.info("*** END PARSE: {time1-time0} ***", 2)
124
125 self.toolcontext.check_errors
126 return mmodules
127 end
128
129 # Return a class named `name' visible by the module `mmodule'.
130 # Visibility in modules is correctly handled.
131 # If no such a class exists, then null is returned.
132 # If more than one class exists, then an error on `anode' is displayed and null is returned.
133 # FIXME: add a way to handle class name conflict
134 fun try_get_mclass_by_name(anode: ANode, mmodule: MModule, name: String): nullable MClass
135 do
136 var classes = model.get_mclasses_by_name(name)
137 if classes == null then
138 return null
139 end
140
141 var res: nullable MClass = null
142 for mclass in classes do
143 if not mmodule.in_importation <= mclass.intro_mmodule then continue
144 if not mmodule.is_visible(mclass.intro_mmodule, mclass.visibility) then continue
145 if res == null then
146 res = mclass
147 else
148 error(anode, "Ambigous class name '{name}'; conflict between {mclass.full_name} and {res.full_name}")
149 return null
150 end
151 end
152 return res
153 end
154
155 # Return a property named `name' on the type `mtype' visible in the module `mmodule'.
156 # Visibility in modules is correctly handled.
157 # Protected properties are returned (it is up to the caller to check and reject protected properties).
158 # If no such a property exists, then null is returned.
159 # If more than one property exists, then an error on `anode' is displayed and null is returned.
160 # FIXME: add a way to handle property name conflict
161 fun try_get_mproperty_by_name2(anode: ANode, mmodule: MModule, mtype: MType, name: String): nullable MProperty
162 do
163 var props = self.model.get_mproperties_by_name(name)
164 if props == null then
165 return null
166 end
167
168 var cache = self.try_get_mproperty_by_name2_cache[mmodule, mtype, name]
169 if cache != null then return cache
170
171 var res: nullable MProperty = null
172 var ress: nullable Array[MProperty] = null
173 for mprop in props do
174 if not mtype.has_mproperty(mmodule, mprop) then continue
175 if not mmodule.is_visible(mprop.intro_mclassdef.mmodule, mprop.visibility) then continue
176 if res == null then
177 res = mprop
178 else
179 var restype = res.intro_mclassdef.bound_mtype
180 var mproptype = mprop.intro_mclassdef.bound_mtype
181 if restype.is_subtype(mmodule, null, mproptype) then
182 # we keep res
183 else if mproptype.is_subtype(mmodule, null, restype) then
184 res = mprop
185 else
186 if ress == null then ress = new Array[MProperty]
187 ress.add(mprop)
188 end
189 end
190 end
191 if ress != null then
192 var restype = res.intro_mclassdef.bound_mtype
193 for mprop in ress do
194 var mproptype = mprop.intro_mclassdef.bound_mtype
195 if not restype.is_subtype(mmodule, null, mproptype) then
196 self.error(anode, "Ambigous property name '{name}' for {mtype}; conflict between {mprop.full_name} and {res.full_name}")
197 return null
198 end
199 end
200 end
201
202 self.try_get_mproperty_by_name2_cache[mmodule, mtype, name] = res
203 return res
204 end
205
206 private var try_get_mproperty_by_name2_cache: HashMap3[MModule, MType, String, nullable MProperty] = new HashMap3[MModule, MType, String, nullable MProperty]
207
208
209 # Alias for try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.mtype, name)
210 fun try_get_mproperty_by_name(anode: ANode, mclassdef: MClassDef, name: String): nullable MProperty
211 do
212 return try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.bound_mtype, name)
213 end
214
215 # The list of directories to search for top level modules
216 # The list is initially set with :
217 # * the toolcontext --path option
218 # * the NIT_PATH environment variable
219 # * some heuristics including the NIT_DIR environment variable and the progname of the process
220 # Path can be added (or removed) by the client
221 var paths: Array[String] = new Array[String]
222
223 # Get a module by its short name; if required, the module is loaded, parsed and its hierarchies computed.
224 # If `mmodule' is set, then the module search starts from it up to the top level (see `paths');
225 # if `mmodule' is null then the module is searched in the top level only.
226 # If no module exists or there is a name conflict, then an error on `anode' is displayed and null is returned.
227 # FIXME: add a way to handle module name conflict
228 fun get_mmodule_by_name(anode: ANode, mmodule: nullable MModule, name: String): nullable MModule
229 do
230 var origmmodule = mmodule
231 var modules = model.get_mmodules_by_name(name)
232
233 var tries = new Array[String]
234
235 var lastmodule = mmodule
236 while mmodule != null do
237 var dirname = mmodule.location.file.filename.dirname
238
239 # Determine the owner
240 var owner: nullable MModule
241 if dirname.basename("") != mmodule.name then
242 owner = mmodule.direct_owner
243 else
244 owner = mmodule
245 end
246
247 # First, try the already known nested modules
248 if modules != null then
249 for candidate in modules do
250 if candidate.direct_owner == owner then
251 return candidate
252 end
253 end
254 end
255
256 # Second, try the directory to find a file
257 var try_file = dirname + "/" + name + ".nit"
258 tries.add try_file
259 if try_file.file_exists then
260 var res = self.load_module(owner, try_file.simplify_path)
261 if res == null then return null # Forward error
262 return res.mmodule.as(not null)
263 end
264
265 # Third, try if the requested module is itself an owner
266 try_file = dirname + "/" + name + "/" + name + ".nit"
267 if try_file.file_exists then
268 var res = self.load_module(owner, try_file.simplify_path)
269 if res == null then return null # Forward error
270 return res.mmodule.as(not null)
271 end
272
273 lastmodule = mmodule
274 mmodule = mmodule.direct_owner
275 end
276
277 if modules != null then
278 for candidate in modules do
279 if candidate.direct_owner == null then
280 return candidate
281 end
282 end
283 end
284
285 # Look at some known directories
286 var lookpaths = self.paths
287
288 # Look in the directory of the last module also (event if not in the path)
289 if lastmodule != null then
290 var dirname = lastmodule.location.file.filename.dirname
291 if dirname.basename("") == lastmodule.name then
292 dirname = dirname.dirname
293 end
294 if not lookpaths.has(dirname) then
295 lookpaths = lookpaths.to_a
296 lookpaths.add(dirname)
297 end
298 end
299
300 var candidate: nullable String = null
301 for dirname in lookpaths do
302 var try_file = (dirname + "/" + name + ".nit").simplify_path
303 tries.add try_file
304 if try_file.file_exists then
305 if candidate == null then
306 candidate = try_file
307 else if candidate != try_file then
308 error(anode, "Error: conflicting module file for {name}: {candidate} {try_file}")
309 end
310 end
311 try_file = (dirname + "/" + name + "/" + name + ".nit").simplify_path
312 if try_file.file_exists then
313 if candidate == null then
314 candidate = try_file
315 else if candidate != try_file then
316 error(anode, "Error: conflicting module file for {name}: {candidate} {try_file}")
317 end
318 end
319 end
320 if candidate == null then
321 if origmmodule != null then
322 error(anode, "Error: cannot find module {name} from {origmmodule}. tried {tries.join(", ")}")
323 else
324 error(anode, "Error: cannot find module {name}. tried {tries.join(", ")}")
325 end
326 return null
327 end
328 var res = self.load_module(mmodule, candidate)
329 if res == null then return null # Forward error
330 return res.mmodule.as(not null)
331 end
332
333 # Try to load a module using a path.
334 # Display an error if there is a problem (IO / lexer / parser) and return null
335 # Note: usually, you do not need this method, use `get_mmodule_by_name` instead.
336 fun load_module(owner: nullable MModule, filename: String): nullable AModule
337 do
338 if not filename.file_exists then
339 self.toolcontext.error(null, "Error: file {filename} not found.")
340 return null
341 end
342
343 var x = if owner != null then owner.to_s else "."
344 self.toolcontext.info("load module {filename} in {x}", 2)
345
346 # Load the file
347 var file = new IFStream.open(filename)
348 var lexer = new Lexer(new SourceFile(filename, file))
349 var parser = new Parser(lexer)
350 var tree = parser.parse
351 file.close
352
353 # Handle lexer and parser error
354 var nmodule = tree.n_base
355 if nmodule == null then
356 var neof = tree.n_eof
357 assert neof isa AError
358 error(neof, neof.message)
359 return null
360 end
361
362 # Check the module name
363 var mod_name = filename.basename(".nit")
364 var decl = nmodule.n_moduledecl
365 if decl == null then
366 #warning(nmodule, "Warning: Missing 'module' keyword") #FIXME: NOT YET FOR COMPATIBILITY
367 else
368 var decl_name = decl.n_name.n_id.text
369 if decl_name != mod_name then
370 error(decl.n_name, "Error: module name missmatch; declared {decl_name} file named {mod_name}")
371 end
372 end
373
374 # Create the module
375 var mmodule = new MModule(model, owner, mod_name, nmodule.location)
376 nmodule.mmodule = mmodule
377 nmodules.add(nmodule)
378 self.mmodule2nmodule[mmodule] = nmodule
379
380 build_module_importation(nmodule)
381
382 return nmodule
383 end
384
385 # Analysis the module importation and fill the module_importation_hierarchy
386 private fun build_module_importation(nmodule: AModule)
387 do
388 if nmodule.is_importation_done then return
389 nmodule.is_importation_done = true
390 var mmodule = nmodule.mmodule.as(not null)
391 var stdimport = true
392 var imported_modules = new Array[MModule]
393 for aimport in nmodule.n_imports do
394 stdimport = false
395 if not aimport isa AStdImport then
396 continue
397 end
398 var mod_name = aimport.n_name.n_id.text
399 var sup = self.get_mmodule_by_name(aimport.n_name, mmodule, mod_name)
400 if sup == null then continue # Skip error
401 imported_modules.add(sup)
402 var mvisibility = aimport.n_visibility.mvisibility
403 mmodule.set_visibility_for(sup, mvisibility)
404 end
405 if stdimport then
406 var mod_name = "standard"
407 var sup = self.get_mmodule_by_name(nmodule, null, mod_name)
408 if sup != null then # Skip error
409 imported_modules.add(sup)
410 mmodule.set_visibility_for(sup, public_visibility)
411 end
412 end
413 self.toolcontext.info("{mmodule} imports {imported_modules.join(", ")}", 3)
414 mmodule.set_imported_mmodules(imported_modules)
415 end
416
417 # All the loaded modules
418 var nmodules: Array[AModule] = new Array[AModule]
419
420 # Register the nmodule associated to each mmodule
421 # FIXME: why not refine the MModule class with a nullable attribute?
422 var mmodule2nmodule: HashMap[MModule, AModule] = new HashMap[MModule, AModule]
423
424 # Helper function to display an error on a node.
425 # Alias for `self.toolcontext.error(n.hot_location, text)'
426 fun error(n: ANode, text: String)
427 do
428 self.toolcontext.error(n.hot_location, text)
429 end
430
431 # Helper function to display a warning on a node.
432 # Alias for: `self.toolcontext.warning(n.hot_location, text)'
433 fun warning(n: ANode, text: String)
434 do
435 self.toolcontext.warning(n.hot_location, text)
436 end
437
438 # Force to get the primitive method named `name' on the type `recv' or do a fatal error on `n'
439 fun force_get_primitive_method(n: ANode, name: String, recv: MType, mmodule: MModule): MMethod
440 do
441 var res = mmodule.try_get_primitive_method(name, recv)
442 if res == null then
443 self.toolcontext.fatal_error(n.hot_location, "Fatal Error: {recv} must have a property named {name}.")
444 abort
445 end
446 return res
447 end
448 end
449
450 redef class AModule
451 # The associated MModule once build by a `ModelBuilder'
452 var mmodule: nullable MModule
453 # Flag that indicate if the importation is already completed
454 var is_importation_done: Bool = false
455 end
456
457 redef class AVisibility
458 # The visibility level associated with the AST node class
459 fun mvisibility: MVisibility is abstract
460 end
461 redef class AIntrudeVisibility
462 redef fun mvisibility do return intrude_visibility
463 end
464 redef class APublicVisibility
465 redef fun mvisibility do return public_visibility
466 end
467 redef class AProtectedVisibility
468 redef fun mvisibility do return protected_visibility
469 end
470 redef class APrivateVisibility
471 redef fun mvisibility do return private_visibility
472 end