modelbuilder: fill the `mdoc` of entities
[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 # what path where tried to display on error message
256 var tries = new Array[String]
257
258 # First, look in groups of the module
259 if mmodule != null then
260 var mgroup = mmodule.mgroup
261 while mgroup != null do
262 var dirname = mgroup.filepath
263 if dirname == null then break # virtual group
264 if dirname.has_suffix(".nit") then break # singleton project
265
266 # Second, try the directory to find a file
267 var try_file = dirname + "/" + name + ".nit"
268 tries.add try_file
269 if try_file.file_exists then
270 var res = self.load_module(try_file.simplify_path)
271 if res == null then return null # Forward error
272 return res.mmodule.as(not null)
273 end
274
275 # Third, try if the requested module is itself a group
276 try_file = dirname + "/" + name + "/" + name + ".nit"
277 if try_file.file_exists then
278 mgroup = get_mgroup(dirname + "/" + name)
279 var res = self.load_module(try_file.simplify_path)
280 if res == null then return null # Forward error
281 return res.mmodule.as(not null)
282 end
283
284 mgroup = mgroup.parent
285 end
286 end
287
288 # Look at some known directories
289 var lookpaths = self.paths
290
291 # Look in the directory of module project also (even if not explicitely in the path)
292 if mmodule != null and mmodule.mgroup != null then
293 # path of the root group
294 var dirname = mmodule.mgroup.mproject.root.filepath
295 if dirname != null then
296 dirname = dirname.join_path("..").simplify_path
297 if not lookpaths.has(dirname) and dirname.file_exists then
298 lookpaths = lookpaths.to_a
299 lookpaths.add(dirname)
300 end
301 end
302 end
303
304 var candidate: nullable String = null
305 for dirname in lookpaths do
306 var try_file = (dirname + "/" + name + ".nit").simplify_path
307 tries.add try_file
308 if try_file.file_exists then
309 if candidate == null then
310 candidate = try_file
311 else if candidate != try_file then
312 # try to disambiguate conflicting modules
313 var abs_candidate = module_absolute_path(candidate)
314 var abs_try_file = module_absolute_path(try_file)
315 if abs_candidate != abs_try_file then
316 error(anode, "Error: conflicting module file for {name}: {candidate} {try_file}")
317 end
318 end
319 end
320 try_file = (dirname + "/" + name + "/" + name + ".nit").simplify_path
321 if try_file.file_exists then
322 if candidate == null then
323 candidate = try_file
324 else if candidate != try_file then
325 # try to disambiguate conflicting modules
326 var abs_candidate = module_absolute_path(candidate)
327 var abs_try_file = module_absolute_path(try_file)
328 if abs_candidate != abs_try_file then
329 error(anode, "Error: conflicting module file for {name}: {candidate} {try_file}")
330 end
331 end
332 end
333 end
334 if candidate == null then
335 if mmodule != null then
336 error(anode, "Error: cannot find module {name} from {mmodule}. tried {tries.join(", ")}")
337 else
338 error(anode, "Error: cannot find module {name}. tried {tries.join(", ")}")
339 end
340 return null
341 end
342 var res = self.load_module(candidate)
343 if res == null then return null # Forward error
344 return res.mmodule.as(not null)
345 end
346
347 # cache for `identify_file` by realpath
348 private var identified_files = new HashMap[String, nullable ModulePath]
349
350 # Identify a source file
351 # Load the associated project and groups if required
352 private fun identify_file(path: String): nullable ModulePath
353 do
354 if not path.file_exists then
355 toolcontext.error(null, "Error: `{path}` does not exists")
356 return null
357 end
358
359 # Fast track, the path is already known
360 var pn = path.basename(".nit")
361 var rp = module_absolute_path(path)
362 if identified_files.has_key(rp) then return identified_files[rp]
363
364 # Search for a group
365 var mgrouppath = path.join_path("..").simplify_path
366 var mgroup = get_mgroup(mgrouppath)
367
368 if mgroup == null then
369 # singleton project
370 var mproject = new MProject(pn, model)
371 mgroup = new MGroup(pn, mproject, null) # same name for the root group
372 mgroup.filepath = path
373 mproject.root = mgroup
374 toolcontext.info("found project `{pn}` at {path}", 2)
375 end
376
377 var res = new ModulePath(pn, path, mgroup)
378
379 identified_files[rp] = res
380 return res
381 end
382
383 # groups by path
384 private var mgroups = new HashMap[String, nullable MGroup]
385
386 # return the mgroup associated to a directory path
387 # if the directory is not a group null is returned
388 private fun get_mgroup(dirpath: String): nullable MGroup
389 do
390 var rdp = module_absolute_path(dirpath)
391 if mgroups.has_key(rdp) then
392 return mgroups[rdp]
393 end
394
395 # Hack, a dir is determined by the presence of a honomymous nit file
396 var pn = rdp.basename(".nit")
397 var mp = dirpath.join_path(pn + ".nit").simplify_path
398
399 if not mp.file_exists then return null
400
401 # check parent directory
402 var parentpath = dirpath.join_path("..").simplify_path
403 var parent = get_mgroup(parentpath)
404
405 var mgroup
406 if parent == null then
407 # no parent, thus new project
408 var mproject = new MProject(pn, model)
409 mgroup = new MGroup(pn, mproject, null) # same name for the root group
410 mproject.root = mgroup
411 toolcontext.info("found project `{mproject}` at {dirpath}", 2)
412 else
413 mgroup = new MGroup(pn, parent.mproject, parent)
414 toolcontext.info("found sub group `{mgroup.full_name}` at {dirpath}", 2)
415 end
416 mgroup.filepath = dirpath
417 mgroups[rdp] = mgroup
418 return mgroup
419 end
420
421 # Transform relative paths (starting with '../') into absolute paths
422 private fun module_absolute_path(path: String): String do
423 return getcwd.join_path(path).simplify_path
424 end
425
426 # Try to load a module AST using a path.
427 # Display an error if there is a problem (IO / lexer / parser) and return null
428 fun load_module_ast(filename: String): nullable AModule
429 do
430 if filename.file_extension != "nit" then
431 self.toolcontext.error(null, "Error: file {filename} is not a valid nit module.")
432 return null
433 end
434 if not filename.file_exists then
435 self.toolcontext.error(null, "Error: file {filename} not found.")
436 return null
437 end
438
439 self.toolcontext.info("load module {filename}", 2)
440
441 # Load the file
442 var file = new IFStream.open(filename)
443 var lexer = new Lexer(new SourceFile(filename, file))
444 var parser = new Parser(lexer)
445 var tree = parser.parse
446 file.close
447 var mod_name = filename.basename(".nit")
448
449 # Handle lexer and parser error
450 var nmodule = tree.n_base
451 if nmodule == null then
452 var neof = tree.n_eof
453 assert neof isa AError
454 error(neof, neof.message)
455 return null
456 end
457
458 return nmodule
459 end
460
461 # Try to load a module and its imported modules using a path.
462 # Display an error if there is a problem (IO / lexer / parser / importation) and return null
463 # Note: usually, you do not need this method, use `get_mmodule_by_name` instead.
464 fun load_module(filename: String): nullable AModule
465 do
466 # Look for the module
467 var file = identify_file(filename)
468 if file == null then return null # forward error
469
470 # Already known and loaded? then return it
471 var mmodule = file.mmodule
472 if mmodule != null then
473 return mmodule2nmodule[mmodule]
474 end
475
476 # Load it manually
477 var nmodule = load_module_ast(filename)
478 if nmodule == null then return null # forward error
479
480 # build the mmodule and load imported modules
481 mmodule = build_a_mmodule(file.mgroup, file.name, nmodule)
482
483 if mmodule == null then return null # forward error
484
485 # Update the file information
486 file.mmodule = mmodule
487
488 # Load imported module
489 build_module_importation(nmodule)
490
491 return nmodule
492 end
493
494 fun load_rt_module(parent: MModule, nmodule: AModule, mod_name: String): nullable AModule
495 do
496 # Create the module
497 var mmodule = new MModule(model, parent.mgroup, mod_name, nmodule.location)
498 nmodule.mmodule = mmodule
499 nmodules.add(nmodule)
500 self.mmodule2nmodule[mmodule] = nmodule
501
502 var imported_modules = new Array[MModule]
503
504 imported_modules.add(parent)
505 mmodule.set_visibility_for(parent, intrude_visibility)
506
507 mmodule.set_imported_mmodules(imported_modules)
508
509 return nmodule
510 end
511
512 # Visit the AST and create the `MModule` object
513 private fun build_a_mmodule(mgroup: nullable MGroup, mod_name: String, nmodule: AModule): nullable MModule
514 do
515 # Check the module name
516 var decl = nmodule.n_moduledecl
517 if decl == null then
518 #warning(nmodule, "Warning: Missing 'module' keyword") #FIXME: NOT YET FOR COMPATIBILITY
519 else
520 var decl_name = decl.n_name.n_id.text
521 if decl_name != mod_name then
522 error(decl.n_name, "Error: module name missmatch; declared {decl_name} file named {mod_name}")
523 end
524 end
525
526 # Create the module
527 var mmodule = new MModule(model, mgroup, mod_name, nmodule.location)
528 nmodule.mmodule = mmodule
529 nmodules.add(nmodule)
530 self.mmodule2nmodule[mmodule] = nmodule
531
532 if decl != null then
533 var ndoc = decl.n_doc
534 if ndoc != null then mmodule.mdoc = ndoc.to_mdoc
535 end
536
537 return mmodule
538 end
539
540 # Analysis the module importation and fill the module_importation_hierarchy
541 private fun build_module_importation(nmodule: AModule)
542 do
543 if nmodule.is_importation_done then return
544 nmodule.is_importation_done = true
545 var mmodule = nmodule.mmodule.as(not null)
546 var stdimport = true
547 var imported_modules = new Array[MModule]
548 for aimport in nmodule.n_imports do
549 stdimport = false
550 if not aimport isa AStdImport then
551 continue
552 end
553 var mod_name = aimport.n_name.n_id.text
554 var sup = self.get_mmodule_by_name(aimport.n_name, mmodule, mod_name)
555 if sup == null then continue # Skip error
556 aimport.mmodule = sup
557 imported_modules.add(sup)
558 var mvisibility = aimport.n_visibility.mvisibility
559 if mvisibility == protected_visibility then
560 error(aimport.n_visibility, "Error: only properties can be protected.")
561 return
562 end
563 if sup == mmodule then
564 error(aimport.n_name, "Error: Dependency loop in module {mmodule}.")
565 end
566 if sup.in_importation < mmodule then
567 error(aimport.n_name, "Error: Dependency loop between modules {mmodule} and {sup}.")
568 return
569 end
570 mmodule.set_visibility_for(sup, mvisibility)
571 end
572 if stdimport then
573 var mod_name = "standard"
574 var sup = self.get_mmodule_by_name(nmodule, null, mod_name)
575 if sup != null then # Skip error
576 imported_modules.add(sup)
577 mmodule.set_visibility_for(sup, public_visibility)
578 end
579 end
580 self.toolcontext.info("{mmodule} imports {imported_modules.join(", ")}", 3)
581 mmodule.set_imported_mmodules(imported_modules)
582 end
583
584 # All the loaded modules
585 var nmodules: Array[AModule] = new Array[AModule]
586
587 # Register the nmodule associated to each mmodule
588 # FIXME: why not refine the `MModule` class with a nullable attribute?
589 var mmodule2nmodule: HashMap[MModule, AModule] = new HashMap[MModule, AModule]
590
591 # Helper function to display an error on a node.
592 # Alias for `self.toolcontext.error(n.hot_location, text)`
593 fun error(n: ANode, text: String)
594 do
595 self.toolcontext.error(n.hot_location, text)
596 end
597
598 # Helper function to display a warning on a node.
599 # Alias for: `self.toolcontext.warning(n.hot_location, text)`
600 fun warning(n: ANode, text: String)
601 do
602 self.toolcontext.warning(n.hot_location, text)
603 end
604
605 # Force to get the primitive method named `name` on the type `recv` or do a fatal error on `n`
606 fun force_get_primitive_method(n: ANode, name: String, recv: MClass, mmodule: MModule): MMethod
607 do
608 var res = mmodule.try_get_primitive_method(name, recv)
609 if res == null then
610 self.toolcontext.fatal_error(n.hot_location, "Fatal Error: {recv} must have a property named {name}.")
611 abort
612 end
613 return res
614 end
615 end
616
617 # placeholder to a module file identified but not always loaded in a project
618 private class ModulePath
619 # The name of the module
620 # (it's the basename of the filepath)
621 var name: String
622
623 # The human path of the module
624 var filepath: String
625
626 # The group (and the project) of the possible module
627 var mgroup: MGroup
628
629 # The loaded module (if any)
630 var mmodule: nullable MModule = null
631
632 redef fun to_s do return filepath
633 end
634
635
636 redef class AStdImport
637 # The imported module once determined
638 var mmodule: nullable MModule = null
639 end
640
641 redef class AModule
642 # The associated MModule once build by a `ModelBuilder`
643 var mmodule: nullable MModule
644 # Flag that indicate if the importation is already completed
645 var is_importation_done: Bool = false
646 end
647
648 redef class AVisibility
649 # The visibility level associated with the AST node class
650 fun mvisibility: MVisibility is abstract
651 end
652 redef class AIntrudeVisibility
653 redef fun mvisibility do return intrude_visibility
654 end
655 redef class APublicVisibility
656 redef fun mvisibility do return public_visibility
657 end
658 redef class AProtectedVisibility
659 redef fun mvisibility do return protected_visibility
660 end
661 redef class APrivateVisibility
662 redef fun mvisibility do return private_visibility
663 end
664
665 redef class ADoc
666 private var mdoc_cache: nullable MDoc
667 fun to_mdoc: MDoc
668 do
669 var res = mdoc_cache
670 if res != null then return res
671 res = new MDoc
672 for c in n_comment do
673 var text = c.text
674 if text.length < 2 then
675 res.content.add ""
676 continue
677 end
678 assert text.chars[0] == '#'
679 if text.chars[1] == ' ' then
680 text = text.substring_from(2) # eat starting `#` and space
681 else
682 text = text.substring_from(1) # eat atarting `#` only
683 end
684 if text.chars.last == '\n' then text = text.substring(0, text.length-1) # drop \n
685 res.content.add(text)
686 end
687 mdoc_cache = res
688 return res
689 end
690 end