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