modelbuilder: add `MGroup::module_paths` to store potential nit files
[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 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 var opt_only_metamodel: OptionBool = new OptionBool("Stop after meta-model processing", "--only-metamodel")
42
43 # Option --only-parse
44 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 fun run_global_phases(mainmodule: MModule)
56 do
57 for phase in phases_list do
58 phase.process_mainmodule(mainmodule)
59 end
60 end
61 end
62
63 redef class Phase
64 # Specific action to execute on the whole program
65 # Called by the `ToolContext::run_global_phases`
66 # @toimplement
67 fun process_mainmodule(mainmodule: MModule) do end
68 end
69
70
71 # A model builder knows how to load nit source files and build the associated model
72 class ModelBuilder
73 # The model where new modules, classes and properties are added
74 var model: Model
75
76 # The toolcontext used to control the interaction with the user (getting options and displaying messages)
77 var toolcontext: ToolContext
78
79 # Run phases on all loaded modules
80 fun run_phases
81 do
82 var mmodules = model.mmodules.to_a
83 model.mmodule_importation_hierarchy.sort(mmodules)
84 var nmodules = new Array[AModule]
85 for mm in mmodules do
86 nmodules.add(mmodule2nmodule[mm])
87 end
88 toolcontext.run_phases(nmodules)
89
90 if toolcontext.opt_only_metamodel.value then
91 self.toolcontext.info("*** ONLY METAMODEL", 1)
92 exit(0)
93 end
94 end
95
96 # Instantiate a modelbuilder for a model and a toolcontext
97 # Important, the options of the toolcontext must be correctly set (parse_option already called)
98 init(model: Model, toolcontext: ToolContext)
99 do
100 self.model = model
101 self.toolcontext = toolcontext
102 assert toolcontext.modelbuilder_real == null
103 toolcontext.modelbuilder_real = self
104
105 # Setup the paths value
106 paths.append(toolcontext.opt_path.value)
107
108 var path_env = "NIT_PATH".environ
109 if not path_env.is_empty then
110 paths.append(path_env.split_with(':'))
111 end
112
113 path_env = "NIT_DIR".environ
114 if not path_env.is_empty then
115 var libname = "{path_env}/lib"
116 if libname.file_exists then paths.add(libname)
117 end
118
119 var libname = "{sys.program_name.dirname}/../lib"
120 if libname.file_exists then paths.add(libname.simplify_path)
121 end
122
123 # Load a bunch of modules.
124 # `modules` can contains filenames or module names.
125 # Imported modules are automatically loaded and modelized.
126 # The result is the corresponding model elements.
127 # Errors and warnings are printed with the toolcontext.
128 #
129 # Note: class and property model element are not analysed.
130 fun parse(modules: Sequence[String]): Array[MModule]
131 do
132 var time0 = get_time
133 # Parse and recursively load
134 self.toolcontext.info("*** PARSE ***", 1)
135 var mmodules = new ArraySet[MModule]
136 for a in modules do
137 var nmodule = self.load_module(a)
138 if nmodule == null then continue # Skip error
139 mmodules.add(nmodule.mmodule.as(not null))
140 end
141 var time1 = get_time
142 self.toolcontext.info("*** END PARSE: {time1-time0} ***", 2)
143
144 self.toolcontext.check_errors
145
146 if toolcontext.opt_only_parse.value then
147 self.toolcontext.info("*** ONLY PARSE...", 1)
148 exit(0)
149 end
150
151 return mmodules.to_a
152 end
153
154 # Return a class named `name` visible by the module `mmodule`.
155 # Visibility in modules is correctly handled.
156 # If no such a class exists, then null is returned.
157 # If more than one class exists, then an error on `anode` is displayed and null is returned.
158 # FIXME: add a way to handle class name conflict
159 fun try_get_mclass_by_name(anode: ANode, mmodule: MModule, name: String): nullable MClass
160 do
161 var classes = model.get_mclasses_by_name(name)
162 if classes == null then
163 return null
164 end
165
166 var res: nullable MClass = null
167 for mclass in classes do
168 if not mmodule.in_importation <= mclass.intro_mmodule then continue
169 if not mmodule.is_visible(mclass.intro_mmodule, mclass.visibility) then continue
170 if res == null then
171 res = mclass
172 else
173 error(anode, "Ambigous class name '{name}'; conflict between {mclass.full_name} and {res.full_name}")
174 return null
175 end
176 end
177 return res
178 end
179
180 # Return a property named `name` on the type `mtype` visible in the module `mmodule`.
181 # Visibility in modules is correctly handled.
182 # Protected properties are returned (it is up to the caller to check and reject protected properties).
183 # If no such a property exists, then null is returned.
184 # If more than one property exists, then an error on `anode` is displayed and null is returned.
185 # FIXME: add a way to handle property name conflict
186 fun try_get_mproperty_by_name2(anode: ANode, mmodule: MModule, mtype: MType, name: String): nullable MProperty
187 do
188 var props = self.model.get_mproperties_by_name(name)
189 if props == null then
190 return null
191 end
192
193 var cache = self.try_get_mproperty_by_name2_cache[mmodule, mtype, name]
194 if cache != null then return cache
195
196 var res: nullable MProperty = null
197 var ress: nullable Array[MProperty] = null
198 for mprop in props do
199 if not mtype.has_mproperty(mmodule, mprop) then continue
200 if not mmodule.is_visible(mprop.intro_mclassdef.mmodule, mprop.visibility) then continue
201 if res == null then
202 res = mprop
203 else
204 var restype = res.intro_mclassdef.bound_mtype
205 var mproptype = mprop.intro_mclassdef.bound_mtype
206 if restype.is_subtype(mmodule, null, mproptype) then
207 # we keep res
208 else if mproptype.is_subtype(mmodule, null, restype) then
209 res = mprop
210 else
211 if ress == null then ress = new Array[MProperty]
212 ress.add(mprop)
213 end
214 end
215 end
216 if ress != null then
217 var restype = res.intro_mclassdef.bound_mtype
218 for mprop in ress do
219 var mproptype = mprop.intro_mclassdef.bound_mtype
220 if not restype.is_subtype(mmodule, null, mproptype) then
221 self.error(anode, "Ambigous property name '{name}' for {mtype}; conflict between {mprop.full_name} and {res.full_name}")
222 return null
223 end
224 end
225 end
226
227 self.try_get_mproperty_by_name2_cache[mmodule, mtype, name] = res
228 return res
229 end
230
231 private var try_get_mproperty_by_name2_cache: HashMap3[MModule, MType, String, nullable MProperty] = new HashMap3[MModule, MType, String, nullable MProperty]
232
233
234 # Alias for try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.mtype, name)
235 fun try_get_mproperty_by_name(anode: ANode, mclassdef: MClassDef, name: String): nullable MProperty
236 do
237 return try_get_mproperty_by_name2(anode, mclassdef.mmodule, mclassdef.bound_mtype, name)
238 end
239
240 # The list of directories to search for top level modules
241 # The list is initially set with :
242 # * the toolcontext --path option
243 # * the NIT_PATH environment variable
244 # * some heuristics including the NIT_DIR environment variable and the progname of the process
245 # Path can be added (or removed) by the client
246 var paths: Array[String] = new Array[String]
247
248 # Get a module by its short name; if required, the module is loaded, parsed and its hierarchies computed.
249 # If `mmodule` is set, then the module search starts from it up to the top level (see `paths`);
250 # if `mmodule` is null then the module is searched in the top level only.
251 # If no module exists or there is a name conflict, then an error on `anode` is displayed and null is returned.
252 # FIXME: add a way to handle module name conflict
253 fun get_mmodule_by_name(anode: ANode, mmodule: nullable MModule, name: String): nullable MModule
254 do
255 # First, look in groups of the module
256 if mmodule != null then
257 var mgroup = mmodule.mgroup
258 while mgroup != null do
259 var dirname = mgroup.filepath
260 if dirname == null then break # virtual group
261 if dirname.has_suffix(".nit") then break # singleton project
262
263 # Second, try the directory to find a file
264 var try_file = dirname + "/" + name + ".nit"
265 if try_file.file_exists then
266 var res = self.load_module(try_file.simplify_path)
267 if res == null then return null # Forward error
268 return res.mmodule.as(not null)
269 end
270
271 # Third, try if the requested module is itself a group
272 try_file = dirname + "/" + name + "/" + name + ".nit"
273 if try_file.file_exists then
274 mgroup = get_mgroup(dirname + "/" + name)
275 var res = self.load_module(try_file.simplify_path)
276 if res == null then return null # Forward error
277 return res.mmodule.as(not null)
278 end
279
280 mgroup = mgroup.parent
281 end
282 end
283
284 # Look at some known directories
285 var lookpaths = self.paths
286
287 # Look in the directory of module project also (even if not explicitely in the path)
288 if mmodule != null and mmodule.mgroup != null then
289 # path of the root group
290 var dirname = mmodule.mgroup.mproject.root.filepath
291 if dirname != null then
292 dirname = dirname.join_path("..").simplify_path
293 if not lookpaths.has(dirname) and dirname.file_exists then
294 lookpaths = lookpaths.to_a
295 lookpaths.add(dirname)
296 end
297 end
298 end
299
300 var candidate = search_module_in_paths(anode.hot_location, name, lookpaths)
301
302 if candidate == null then
303 if mmodule != null then
304 error(anode, "Error: cannot find module {name} from {mmodule}. tried {lookpaths.join(", ")}")
305 else
306 error(anode, "Error: cannot find module {name}. tried {lookpaths.join(", ")}")
307 end
308 return null
309 end
310 var res = self.load_module(candidate)
311 if res == null then return null # Forward error
312 return res.mmodule.as(not null)
313 end
314
315 # Search a module `name` from path `lookpaths`.
316 # If found, the path of the file is returned
317 private fun search_module_in_paths(location: nullable Location, name: String, lookpaths: Collection[String]): nullable String
318 do
319 var candidate: nullable String = null
320 for dirname in lookpaths do
321 var try_file = (dirname + "/" + name + ".nit").simplify_path
322 if try_file.file_exists then
323 if candidate == null then
324 candidate = try_file
325 else if candidate != try_file then
326 # try to disambiguate conflicting modules
327 var abs_candidate = module_absolute_path(candidate)
328 var abs_try_file = module_absolute_path(try_file)
329 if abs_candidate != abs_try_file then
330 toolcontext.error(location, "Error: conflicting module file for {name}: {candidate} {try_file}")
331 end
332 end
333 end
334 try_file = (dirname + "/" + name + "/" + name + ".nit").simplify_path
335 if try_file.file_exists then
336 if candidate == null then
337 candidate = try_file
338 else if candidate != try_file then
339 # try to disambiguate conflicting modules
340 var abs_candidate = module_absolute_path(candidate)
341 var abs_try_file = module_absolute_path(try_file)
342 if abs_candidate != abs_try_file then
343 toolcontext.error(location, "Error: conflicting module file for {name}: {candidate} {try_file}")
344 end
345 end
346 end
347 end
348 return candidate
349 end
350
351 # cache for `identify_file` by realpath
352 private var identified_files = new HashMap[String, nullable ModulePath]
353
354 # Identify a source file
355 # Load the associated project and groups if required
356 private fun identify_file(path: String): nullable ModulePath
357 do
358 # special case for not a nit file
359 if path.file_extension != "nit" then
360 # search in known -I paths
361 var candidate = search_module_in_paths(null, path, self.paths)
362
363 # Found nothins? maybe it is a group...
364 if candidate == null and path.file_exists then
365 var mgroup = get_mgroup(path)
366 if mgroup != null then
367 var owner_path = mgroup.filepath.join_path(mgroup.name + ".nit")
368 if owner_path.file_exists then candidate = owner_path
369 end
370 end
371
372 if candidate == null then
373 toolcontext.error(null, "Error: cannot find module `{path}`.")
374 return null
375 end
376 path = candidate
377 end
378
379 # Fast track, the path is already known
380 var pn = path.basename(".nit")
381 var rp = module_absolute_path(path)
382 if identified_files.has_key(rp) then return identified_files[rp]
383
384 # Search for a group
385 var mgrouppath = path.join_path("..").simplify_path
386 var mgroup = get_mgroup(mgrouppath)
387
388 if mgroup == null then
389 # singleton project
390 var mproject = new MProject(pn, model)
391 mgroup = new MGroup(pn, mproject, null) # same name for the root group
392 mgroup.filepath = path
393 mproject.root = mgroup
394 toolcontext.info("found project `{pn}` at {path}", 2)
395 end
396
397 var res = new ModulePath(pn, path, mgroup)
398 mgroup.module_paths.add(res)
399
400 identified_files[rp] = res
401 return res
402 end
403
404 # groups by path
405 private var mgroups = new HashMap[String, nullable MGroup]
406
407 # return the mgroup associated to a directory path
408 # if the directory is not a group null is returned
409 private fun get_mgroup(dirpath: String): nullable MGroup
410 do
411 var rdp = module_absolute_path(dirpath)
412 if mgroups.has_key(rdp) then
413 return mgroups[rdp]
414 end
415
416 # Hack, a dir is determined by the presence of a honomymous nit file
417 var pn = rdp.basename(".nit")
418 var mp = dirpath.join_path(pn + ".nit").simplify_path
419
420 if not mp.file_exists then return null
421
422 # check parent directory
423 var parentpath = dirpath.join_path("..").simplify_path
424 var parent = get_mgroup(parentpath)
425
426 var mgroup
427 if parent == null then
428 # no parent, thus new project
429 var mproject = new MProject(pn, model)
430 mgroup = new MGroup(pn, mproject, null) # same name for the root group
431 mproject.root = mgroup
432 toolcontext.info("found project `{mproject}` at {dirpath}", 2)
433 else
434 mgroup = new MGroup(pn, parent.mproject, parent)
435 toolcontext.info("found sub group `{mgroup.full_name}` at {dirpath}", 2)
436 end
437 mgroup.filepath = dirpath
438 mgroups[rdp] = mgroup
439 return mgroup
440 end
441
442 # Transform relative paths (starting with '../') into absolute paths
443 private fun module_absolute_path(path: String): String do
444 return getcwd.join_path(path).simplify_path
445 end
446
447 # Try to load a module AST using a path.
448 # Display an error if there is a problem (IO / lexer / parser) and return null
449 fun load_module_ast(filename: String): nullable AModule
450 do
451 if filename.file_extension != "nit" then
452 self.toolcontext.error(null, "Error: file {filename} is not a valid nit module.")
453 return null
454 end
455 if not filename.file_exists then
456 self.toolcontext.error(null, "Error: file {filename} not found.")
457 return null
458 end
459
460 self.toolcontext.info("load module {filename}", 2)
461
462 # Load the file
463 var file = new IFStream.open(filename)
464 var lexer = new Lexer(new SourceFile(filename, file))
465 var parser = new Parser(lexer)
466 var tree = parser.parse
467 file.close
468 var mod_name = filename.basename(".nit")
469
470 # Handle lexer and parser error
471 var nmodule = tree.n_base
472 if nmodule == null then
473 var neof = tree.n_eof
474 assert neof isa AError
475 error(neof, neof.message)
476 return null
477 end
478
479 return nmodule
480 end
481
482 # Try to load a module and its imported modules using a path.
483 # Display an error if there is a problem (IO / lexer / parser / importation) and return null
484 # Note: usually, you do not need this method, use `get_mmodule_by_name` instead.
485 fun load_module(filename: String): nullable AModule
486 do
487 # Look for the module
488 var file = identify_file(filename)
489 if file == null then return null # forward error
490
491 # Already known and loaded? then return it
492 var mmodule = file.mmodule
493 if mmodule != null then
494 return mmodule2nmodule[mmodule]
495 end
496
497 # Load it manually
498 var nmodule = load_module_ast(file.filepath)
499 if nmodule == null then return null # forward error
500
501 # build the mmodule and load imported modules
502 mmodule = build_a_mmodule(file.mgroup, file.name, nmodule)
503
504 if mmodule == null then return null # forward error
505
506 # Update the file information
507 file.mmodule = mmodule
508
509 # Load imported module
510 build_module_importation(nmodule)
511
512 return nmodule
513 end
514
515 fun load_rt_module(parent: MModule, nmodule: AModule, mod_name: String): nullable AModule
516 do
517 # Create the module
518 var mmodule = new MModule(model, parent.mgroup, mod_name, nmodule.location)
519 nmodule.mmodule = mmodule
520 nmodules.add(nmodule)
521 self.mmodule2nmodule[mmodule] = nmodule
522
523 var imported_modules = new Array[MModule]
524
525 imported_modules.add(parent)
526 mmodule.set_visibility_for(parent, intrude_visibility)
527
528 mmodule.set_imported_mmodules(imported_modules)
529
530 return nmodule
531 end
532
533 # Visit the AST and create the `MModule` object
534 private fun build_a_mmodule(mgroup: nullable MGroup, mod_name: String, nmodule: AModule): nullable MModule
535 do
536 # Check the module name
537 var decl = nmodule.n_moduledecl
538 if decl == null then
539 #warning(nmodule, "Warning: Missing 'module' keyword") #FIXME: NOT YET FOR COMPATIBILITY
540 else
541 var decl_name = decl.n_name.n_id.text
542 if decl_name != mod_name then
543 error(decl.n_name, "Error: module name missmatch; declared {decl_name} file named {mod_name}")
544 end
545 end
546
547 # Create the module
548 var mmodule = new MModule(model, mgroup, mod_name, nmodule.location)
549 nmodule.mmodule = mmodule
550 nmodules.add(nmodule)
551 self.mmodule2nmodule[mmodule] = nmodule
552
553 return mmodule
554 end
555
556 # Analysis the module importation and fill the module_importation_hierarchy
557 private fun build_module_importation(nmodule: AModule)
558 do
559 if nmodule.is_importation_done then return
560 nmodule.is_importation_done = true
561 var mmodule = nmodule.mmodule.as(not null)
562 var stdimport = true
563 var imported_modules = new Array[MModule]
564 for aimport in nmodule.n_imports do
565 stdimport = false
566 if not aimport isa AStdImport then
567 continue
568 end
569 var mod_name = aimport.n_name.n_id.text
570 var sup = self.get_mmodule_by_name(aimport.n_name, mmodule, mod_name)
571 if sup == null then continue # Skip error
572 aimport.mmodule = sup
573 imported_modules.add(sup)
574 var mvisibility = aimport.n_visibility.mvisibility
575 if mvisibility == protected_visibility then
576 error(aimport.n_visibility, "Error: only properties can be protected.")
577 return
578 end
579 if sup == mmodule then
580 error(aimport.n_name, "Error: Dependency loop in module {mmodule}.")
581 end
582 if sup.in_importation < mmodule then
583 error(aimport.n_name, "Error: Dependency loop between modules {mmodule} and {sup}.")
584 return
585 end
586 mmodule.set_visibility_for(sup, mvisibility)
587 end
588 if stdimport then
589 var mod_name = "standard"
590 var sup = self.get_mmodule_by_name(nmodule, null, mod_name)
591 if sup != null then # Skip error
592 imported_modules.add(sup)
593 mmodule.set_visibility_for(sup, public_visibility)
594 end
595 end
596 self.toolcontext.info("{mmodule} imports {imported_modules.join(", ")}", 3)
597 mmodule.set_imported_mmodules(imported_modules)
598 end
599
600 # All the loaded modules
601 var nmodules: Array[AModule] = new Array[AModule]
602
603 # Register the nmodule associated to each mmodule
604 # FIXME: why not refine the `MModule` class with a nullable attribute?
605 var mmodule2nmodule: HashMap[MModule, AModule] = new HashMap[MModule, AModule]
606
607 # Helper function to display an error on a node.
608 # Alias for `self.toolcontext.error(n.hot_location, text)`
609 fun error(n: ANode, text: String)
610 do
611 self.toolcontext.error(n.hot_location, text)
612 end
613
614 # Helper function to display a warning on a node.
615 # Alias for: `self.toolcontext.warning(n.hot_location, text)`
616 fun warning(n: ANode, text: String)
617 do
618 self.toolcontext.warning(n.hot_location, text)
619 end
620
621 # Force to get the primitive method named `name` on the type `recv` or do a fatal error on `n`
622 fun force_get_primitive_method(n: ANode, name: String, recv: MClass, mmodule: MModule): MMethod
623 do
624 var res = mmodule.try_get_primitive_method(name, recv)
625 if res == null then
626 self.toolcontext.fatal_error(n.hot_location, "Fatal Error: {recv} must have a property named {name}.")
627 abort
628 end
629 return res
630 end
631 end
632
633 # placeholder to a module file identified but not always loaded in a project
634 private class ModulePath
635 # The name of the module
636 # (it's the basename of the filepath)
637 var name: String
638
639 # The human path of the module
640 var filepath: String
641
642 # The group (and the project) of the possible module
643 var mgroup: MGroup
644
645 # The loaded module (if any)
646 var mmodule: nullable MModule = null
647
648 redef fun to_s do return filepath
649 end
650
651 redef class MGroup
652 # modules paths associated with the group
653 private var module_paths = new Array[ModulePath]
654 end
655
656 redef class AStdImport
657 # The imported module once determined
658 var mmodule: nullable MModule = null
659 end
660
661 redef class AModule
662 # The associated MModule once build by a `ModelBuilder`
663 var mmodule: nullable MModule
664 # Flag that indicate if the importation is already completed
665 var is_importation_done: Bool = false
666 end
667
668 redef class AVisibility
669 # The visibility level associated with the AST node class
670 fun mvisibility: MVisibility is abstract
671 end
672 redef class AIntrudeVisibility
673 redef fun mvisibility do return intrude_visibility
674 end
675 redef class APublicVisibility
676 redef fun mvisibility do return public_visibility
677 end
678 redef class AProtectedVisibility
679 redef fun mvisibility do return protected_visibility
680 end
681 redef class APrivateVisibility
682 redef fun mvisibility do return private_visibility
683 end