modelbuilder: in load_rt_module, rename `owner` to `parent`
[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(null, 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 var origmmodule = mmodule
256 var modules = model.get_mmodules_by_name(name)
257
258 var tries = new Array[String]
259
260 var lastmodule = mmodule
261 while mmodule != null do
262 var dirname = mmodule.location.file.filename.dirname
263
264 # Determine the owner
265 var owner: nullable MModule
266 if dirname.basename("") != mmodule.name then
267 owner = mmodule.direct_owner
268 else
269 owner = mmodule
270 end
271
272 # First, try the already known nested modules
273 if modules != null then
274 for candidate in modules do
275 if candidate.direct_owner == owner then
276 return candidate
277 end
278 end
279 end
280
281 # Second, try the directory to find a file
282 var try_file = dirname + "/" + name + ".nit"
283 tries.add try_file
284 if try_file.file_exists then
285 var res = self.load_module(owner, try_file.simplify_path)
286 if res == null then return null # Forward error
287 return res.mmodule.as(not null)
288 end
289
290 # Third, try if the requested module is itself an owner
291 try_file = dirname + "/" + name + "/" + name + ".nit"
292 if try_file.file_exists then
293 var res = self.load_module(owner, try_file.simplify_path)
294 if res == null then return null # Forward error
295 return res.mmodule.as(not null)
296 end
297
298 lastmodule = mmodule
299 mmodule = mmodule.direct_owner
300 end
301
302 if modules != null then
303 for candidate in modules do
304 if candidate.direct_owner == null then
305 return candidate
306 end
307 end
308 end
309
310 # Look at some known directories
311 var lookpaths = self.paths
312
313 # Look in the directory of the last module also (event if not in the path)
314 if lastmodule != null then
315 var dirname = lastmodule.location.file.filename.dirname
316 if dirname.basename("") == lastmodule.name then
317 dirname = dirname.dirname
318 end
319 if not lookpaths.has(dirname) then
320 lookpaths = lookpaths.to_a
321 lookpaths.add(dirname)
322 end
323 end
324
325 var candidate: nullable String = null
326 for dirname in lookpaths do
327 var try_file = (dirname + "/" + name + ".nit").simplify_path
328 tries.add try_file
329 if try_file.file_exists then
330 if candidate == null then
331 candidate = try_file
332 else if candidate != try_file then
333 # try to disambiguate conflicting modules
334 var abs_candidate = module_absolute_path(candidate)
335 var abs_try_file = module_absolute_path(try_file)
336 if abs_candidate != abs_try_file then
337 error(anode, "Error: conflicting module file for {name}: {candidate} {try_file}")
338 end
339 end
340 end
341 try_file = (dirname + "/" + name + "/" + name + ".nit").simplify_path
342 if try_file.file_exists then
343 if candidate == null then
344 candidate = try_file
345 else if candidate != try_file then
346 # try to disambiguate conflicting modules
347 var abs_candidate = module_absolute_path(candidate)
348 var abs_try_file = module_absolute_path(try_file)
349 if abs_candidate != abs_try_file then
350 error(anode, "Error: conflicting module file for {name}: {candidate} {try_file}")
351 end
352 end
353 end
354 end
355 if candidate == null then
356 if origmmodule != null then
357 error(anode, "Error: cannot find module {name} from {origmmodule}. tried {tries.join(", ")}")
358 else
359 error(anode, "Error: cannot find module {name}. tried {tries.join(", ")}")
360 end
361 return null
362 end
363 var res = self.load_module(mmodule, candidate)
364 if res == null then return null # Forward error
365 return res.mmodule.as(not null)
366 end
367
368 # Transform relative paths (starting with '../') into absolute paths
369 private fun module_absolute_path(path: String): String do
370 if path.has_prefix("..") then
371 return getcwd.join_path(path).simplify_path
372 end
373 return path
374 end
375
376 # loaded module by absolute path
377 private var loaded_nmodules = new HashMap[String, AModule]
378
379 # Try to load a module AST using a path.
380 # Display an error if there is a problem (IO / lexer / parser) and return null
381 fun load_module_ast(filename: String): nullable AModule
382 do
383 if filename.file_extension != "nit" then
384 self.toolcontext.error(null, "Error: file {filename} is not a valid nit module.")
385 return null
386 end
387 if not filename.file_exists then
388 self.toolcontext.error(null, "Error: file {filename} not found.")
389 return null
390 end
391
392 var module_path = module_absolute_path(filename)
393 if loaded_nmodules.keys.has(module_path) then
394 return loaded_nmodules[module_path]
395 end
396
397 self.toolcontext.info("load module {filename}", 2)
398
399 # Load the file
400 var file = new IFStream.open(filename)
401 var lexer = new Lexer(new SourceFile(filename, file))
402 var parser = new Parser(lexer)
403 var tree = parser.parse
404 file.close
405 var mod_name = filename.basename(".nit")
406
407 # Handle lexer and parser error
408 var nmodule = tree.n_base
409 if nmodule == null then
410 var neof = tree.n_eof
411 assert neof isa AError
412 error(neof, neof.message)
413 return null
414 end
415
416 loaded_nmodules[module_path] = nmodule
417 return nmodule
418 end
419
420 # Try to load a module and its imported modules using a path.
421 # Display an error if there is a problem (IO / lexer / parser / importation) and return null
422 # Note: usually, you do not need this method, use `get_mmodule_by_name` instead.
423 fun load_module(owner: nullable MModule, filename: String): nullable AModule
424 do
425 var nmodule = load_module_ast(filename)
426 if nmodule == null then return null # forward error
427
428 var mmodule = nmodule.mmodule
429 if mmodule != null then return nmodule
430
431 var mod_name = filename.basename(".nit")
432 mmodule = build_a_mmodule(owner, mod_name, nmodule)
433 if mmodule == null then return null
434
435 return nmodule
436 end
437
438 fun load_rt_module(parent: MModule, nmodule: AModule, mod_name: String): nullable AModule
439 do
440 # Create the module
441 var mmodule = new MModule(model, parent, mod_name, nmodule.location)
442 nmodule.mmodule = mmodule
443 nmodules.add(nmodule)
444 self.mmodule2nmodule[mmodule] = nmodule
445
446 var imported_modules = new Array[MModule]
447
448 imported_modules.add(parent)
449 mmodule.set_visibility_for(parent, intrude_visibility)
450
451 mmodule.set_imported_mmodules(imported_modules)
452
453 return nmodule
454 end
455
456 # Visit the AST and create the `MModule` object
457 # Then, recursively load imported modules
458 private fun build_a_mmodule(owner: nullable MModule, mod_name: String, nmodule: AModule): nullable MModule
459 do
460 # Check the module name
461 var decl = nmodule.n_moduledecl
462 if decl == null then
463 #warning(nmodule, "Warning: Missing 'module' keyword") #FIXME: NOT YET FOR COMPATIBILITY
464 else
465 var decl_name = decl.n_name.n_id.text
466 if decl_name != mod_name then
467 error(decl.n_name, "Error: module name missmatch; declared {decl_name} file named {mod_name}")
468 end
469 end
470
471 # Create the module
472 var mmodule = new MModule(model, owner, mod_name, nmodule.location)
473 nmodule.mmodule = mmodule
474 nmodules.add(nmodule)
475 self.mmodule2nmodule[mmodule] = nmodule
476
477 build_module_importation(nmodule)
478
479 return mmodule
480 end
481
482 # Analysis the module importation and fill the module_importation_hierarchy
483 private fun build_module_importation(nmodule: AModule)
484 do
485 if nmodule.is_importation_done then return
486 nmodule.is_importation_done = true
487 var mmodule = nmodule.mmodule.as(not null)
488 var stdimport = true
489 var imported_modules = new Array[MModule]
490 for aimport in nmodule.n_imports do
491 stdimport = false
492 if not aimport isa AStdImport then
493 continue
494 end
495 var mod_name = aimport.n_name.n_id.text
496 var sup = self.get_mmodule_by_name(aimport.n_name, mmodule, mod_name)
497 if sup == null then continue # Skip error
498 aimport.mmodule = sup
499 imported_modules.add(sup)
500 var mvisibility = aimport.n_visibility.mvisibility
501 if mvisibility == protected_visibility then
502 error(aimport.n_visibility, "Error: only properties can be protected.")
503 return
504 end
505 mmodule.set_visibility_for(sup, mvisibility)
506 end
507 if stdimport then
508 var mod_name = "standard"
509 var sup = self.get_mmodule_by_name(nmodule, null, mod_name)
510 if sup != null then # Skip error
511 imported_modules.add(sup)
512 mmodule.set_visibility_for(sup, public_visibility)
513 end
514 end
515 self.toolcontext.info("{mmodule} imports {imported_modules.join(", ")}", 3)
516 mmodule.set_imported_mmodules(imported_modules)
517 end
518
519 # All the loaded modules
520 var nmodules: Array[AModule] = new Array[AModule]
521
522 # Register the nmodule associated to each mmodule
523 # FIXME: why not refine the `MModule` class with a nullable attribute?
524 var mmodule2nmodule: HashMap[MModule, AModule] = new HashMap[MModule, AModule]
525
526 # Helper function to display an error on a node.
527 # Alias for `self.toolcontext.error(n.hot_location, text)`
528 fun error(n: ANode, text: String)
529 do
530 self.toolcontext.error(n.hot_location, text)
531 end
532
533 # Helper function to display a warning on a node.
534 # Alias for: `self.toolcontext.warning(n.hot_location, text)`
535 fun warning(n: ANode, text: String)
536 do
537 self.toolcontext.warning(n.hot_location, text)
538 end
539
540 # Force to get the primitive method named `name` on the type `recv` or do a fatal error on `n`
541 fun force_get_primitive_method(n: ANode, name: String, recv: MClass, mmodule: MModule): MMethod
542 do
543 var res = mmodule.try_get_primitive_method(name, recv)
544 if res == null then
545 self.toolcontext.fatal_error(n.hot_location, "Fatal Error: {recv} must have a property named {name}.")
546 abort
547 end
548 return res
549 end
550 end
551
552 redef class AStdImport
553 # The imported module once determined
554 var mmodule: nullable MModule = null
555 end
556
557 redef class AModule
558 # The associated MModule once build by a `ModelBuilder`
559 var mmodule: nullable MModule
560 # Flag that indicate if the importation is already completed
561 var is_importation_done: Bool = false
562 end
563
564 redef class AVisibility
565 # The visibility level associated with the AST node class
566 fun mvisibility: MVisibility is abstract
567 end
568 redef class AIntrudeVisibility
569 redef fun mvisibility do return intrude_visibility
570 end
571 redef class APublicVisibility
572 redef fun mvisibility do return public_visibility
573 end
574 redef class AProtectedVisibility
575 redef fun mvisibility do return protected_visibility
576 end
577 redef class APrivateVisibility
578 redef fun mvisibility do return private_visibility
579 end