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