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