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