1 /// Author: Aziz Köksal
2 /// License: GPL3
3 /// $(Maturity high)
4 module dil.ModuleManager;
5 
6 import dil.semantic.Module,
7        dil.semantic.Package,
8        dil.semantic.Symbol;
9 import dil.lexer.Token;
10 import dil.i18n.Messages;
11 import dil.Compilation,
12        dil.Diagnostics,
13        dil.String;
14 import util.Path;
15 import common;
16 
17 import std.algorithm : sort;
18 import std.range : assumeSorted;
19 
20 /// Manages loaded modules in various tables.
21 class ModuleManager
22 {
23   /// The root package. Contains all other modules and packages.
24   Package rootPackage;
25   /// Maps full package names to packages. E.g.: dil.ast
26   Package[hash_t] packageTable;
27   /// Maps FQN paths to modules. E.g.: dil/ast/Node
28   Module[hash_t] moduleFQNPathTable;
29   /// Maps absolute file paths to modules. E.g.: /home/user/dil/src/main.d
30   Module[hash_t] absFilePathTable;
31   /// Loaded modules in sequential order.
32   Module[] loadedModules;
33   /// Loaded modules which are ordered according to the number of
34   /// import statements in each module (ascending order.)
35   Module[] orderedModules;
36   /// Provides tables and compiler variables.
37   CompilationContext cc;
38 
39   /// Constructs a ModuleManager object.
40   this(CompilationContext cc)
41   {
42     this.rootPackage = new Package(null, cc.tables.idents);
43     packageTable[0] = this.rootPackage;
44     assert(hashOf("") == 0);
45     this.cc = cc;
46   }
47 
48   /// Looks up a module by its file path. E.g.: "src/dil/ModuleManager.d"
49   /// Relative paths are made absolute.
50   Module moduleByPath(cstring moduleFilePath)
51   {
52     auto absFilePath = absolutePath(moduleFilePath);
53     if (auto existingModule = hashOf(absFilePath) in absFilePathTable)
54       return *existingModule;
55     return null;
56   }
57 
58   /// Looks up a module by its f.q.n. path. E.g.: "dil/ModuleManager"
59   Module moduleByFQN(cstring moduleFQNPath)
60   {
61     if (auto existingModule = hashOf(moduleFQNPath) in moduleFQNPathTable)
62       return *existingModule;
63     return null;
64   }
65 
66   /// Loads and parses a module given a file path.
67   /// Returns: A new Module instance or an existing one from the table.
68   Module loadModuleFile(cstring moduleFilePath)
69   {
70     if (auto existingModule = moduleByPath(moduleFilePath))
71       return existingModule;
72 
73     if (Path(moduleFilePath).isFolder)
74     {
75       auto msg = cc.diag.formatMsg(MID.ModulePathIsFolder, moduleFilePath);
76       cc.diag ~= new LexerError(new Location(moduleFilePath, 0), msg);
77       return null;
78     }
79 
80     // Create a new module.
81     auto newModule = new Module(moduleFilePath, cc);
82     newModule.parse();
83 
84     addModule(newModule);
85 
86     return newModule;
87   }
88 
89   /// Loads a module given an FQN path. Searches import paths.
90   Module loadModule(cstring moduleFQNPath)
91   {
92     // Look up in table if the module is already loaded.
93     if (auto existingModule = moduleByFQN(moduleFQNPath))
94       return existingModule;
95 
96     // Locate the module in the file system.
97     auto moduleFilePath = findModuleFile(moduleFQNPath);
98     if (!moduleFilePath.length)
99       return null; // No module found.
100 
101     // Load the module file.
102     auto modul = loadModuleFile(moduleFilePath);
103     if (!modul)
104       return null;
105 
106     auto packageFQN = getPackageFQN(moduleFQNPath);
107     if (getPackageFQN(modul.getFQNPath()) != packageFQN)
108       // Error: the requested module is not in the correct package.
109       error(modul, MID.ModuleNotInPackage, packageFQN);
110 
111     return modul;
112   }
113 
114   /// Inserts the given module into the tables.
115   void addModule(Module newModule)
116   {
117     auto absFilePath = absolutePath(newModule.filePath());
118 
119     auto moduleFQNPath = newModule.getFQNPath();
120     auto fqnPathHash = hashOf(moduleFQNPath);
121 
122     if (auto existingModule = fqnPathHash in moduleFQNPathTable)
123       // Error: two module files have the same f.q. module name.
124       return error(newModule,
125         MID.ConflictingModuleFiles, newModule.filePath());
126 
127     // Insert into the tables.
128     moduleFQNPathTable[fqnPathHash] = newModule;
129     absFilePathTable[hashOf(absFilePath)] = newModule;
130     loadedModules ~= newModule;
131     newModule.ID = loadedModules.length;
132     insertOrdered(newModule);
133 
134     auto nrOfPckgs = packageTable.length; // Remember for error checking.
135     // Add the module to its package.
136     auto pckg = getPackage(newModule.packageName);
137     pckg.add(newModule);
138 
139     if (auto p = hashOf(newModule.getFQN()) in packageTable)
140       // Error: module and package share the same name.
141       // Happens when: "src/dil/module.d", "src/dil.d"
142       // There's a package dil and a module dil.
143       return error(newModule,
144         MID.ConflictingModuleAndPackage, newModule.getFQN());
145 
146     if (nrOfPckgs != packageTable.length) // Were new packages added?
147     { // Check whether any new package is in conflict with an existing module.
148       uint i; // Used to get the exact package in a module declaration.
149       auto p = newModule.parent.to!(Package);
150       for (; p.parent; p = p.parentPackage()) // Go up until root package.
151       {
152         i++;
153         auto pckgFQN = p.getFQN(); // E.g.: dil.ast
154         auto pckgFQNPath = pckgFQN.replace('.', dirSep); // E.g.: dil/ast
155         if (hashOf(pckgFQNPath) in moduleFQNPathTable)
156           // Error: package and module share the same name.
157           return error(newModule.moduleDecl.packages[$-i], newModule,
158             MID.ConflictingPackageAndModule, pckgFQN);
159       }
160     }
161   }
162 
163   /// Compares the number of imports of two modules.
164   /// Returns: true if a imports less than b.
165   static bool compareImports(Module a, Module b)
166   {
167     return a.imports.length < b.imports.length;
168   }
169 
170   /// Insert a module into the ordered list.
171   void insertOrdered(Module newModule)
172   {
173     auto sorted = orderedModules.assumeSorted!compareImports();
174     auto i = sorted.lowerBound(newModule).length;
175     if (i == orderedModules.length)
176       orderedModules ~= newModule;
177     else
178       orderedModules = orderedModules[0..i] ~ newModule ~ orderedModules[i..$];
179   }
180 
181   /// Returns the package given a f.q. package name.
182   /// Returns the root package for an empty string.
183   Package getPackage(cstring pckgFQN)
184   {
185     auto fqnHash = hashOf(pckgFQN);
186     if (auto existingPackage = fqnHash in packageTable)
187       return *existingPackage;
188 
189     cstring prevFQN, lastPckgName;
190     // E.g.: pckgFQN = 'dil.ast', prevFQN = 'dil', lastPckgName = 'ast'
191     splitPackageFQN(pckgFQN, prevFQN, lastPckgName);
192     // Recursively build package hierarchy.
193     auto parentPckg = getPackage(prevFQN); // E.g.: 'dil'
194 
195     // Create a new package.
196     auto pckg = new Package(lastPckgName, cc.tables.idents); // E.g.: 'ast'
197     parentPckg.add(pckg); // 'dil'.add('ast')
198 
199     // Insert the package into the table.
200     packageTable[fqnHash] = pckg;
201 
202     return pckg;
203   }
204 
205   /// Splits e.g. 'dil.ast.xyz' into 'dil.ast' and 'xyz'.
206   /// Params:
207   ///   pckgFQN = The full package name to be split.
208   ///   prevFQN = Set to 'dil.ast' in the example.
209   ///   lastName = The last package name; set to 'xyz' in the example.
210   void splitPackageFQN(cstring pckgFQN,
211     out cstring prevFQN, out cstring lastName)
212   {
213     auto s = String(pckgFQN).rpartition('.');
214     prevFQN = s[0][];
215     lastName = s[1][];
216   }
217 
218   /// Returns e.g. 'dil.ast' for 'dil/ast/Node'.
219   static char[] getPackageFQN(cstring moduleFQNPath)
220   {
221     return String(moduleFQNPath).sub(dirSep, '.').rpartition('.')[0][];
222   }
223 
224   /// Searches for a module in the file system looking in importPaths.
225   /// Returns: The file path to the module, or null if it wasn't found.
226   static cstring findModuleFile(cstring moduleFQNPath, cstring[] importPaths)
227   {
228     auto filePath = Path();
229     foreach (importPath; importPaths)
230     { // E.g.: "path/to/src" ~ "/" ~ "dil/ast/Node" ~ ".d"
231       (filePath.set(importPath) /= moduleFQNPath) ~= ".d";
232       // or: "src/dil/ast/Node.di"
233       if (filePath.exists() || (filePath~="i").exists())
234         return filePath[];
235     }
236     return null;
237   }
238 
239   /// ditto
240   cstring findModuleFile(cstring moduleFQNPath)
241   {
242     return findModuleFile(moduleFQNPath, cc.importPaths);
243   }
244 
245   /// A predicate for sorting symbols in ascending order.
246   /// Compares symbol names ignoring case.
247   static bool compareSymbolNames(Symbol a, Symbol b)
248   {
249     return String(a.name.str).icmp(b.name.str) < 0;
250   }
251 
252   /// Sorts the the subpackages and submodules of pckg.
253   void sortPackageTree(Package pckg)
254   {
255     pckg.packages.sort!(compareSymbolNames)();
256     pckg.modules.sort!(compareSymbolNames)();
257     foreach (subpckg; pckg.packages)
258       sortPackageTree(subpckg);
259   }
260 
261   /// Calls sortPackageTree() with this.rootPackage.
262   void sortPackageTree()
263   {
264     sortPackageTree(rootPackage);
265   }
266 
267   /// Returns a normalized, absolute path.
268   static cstring absolutePath(cstring path)
269   {
270     return Path(path).absolute().normalize()[];
271   }
272 
273   /// Reports an error.
274   void error(Module modul, MID mid, ...)
275   {
276     auto msg = cc.diag.formatMsg(mid, _arguments, _argptr);
277     auto loc = modul.loc.t.getErrorLocation(modul.filePath);
278     cc.diag ~= new SemanticError(loc, msg);
279   }
280 
281   /// Reports an error.
282   void error(Token* locTok, Module modul, MID mid, ...)
283   {
284     auto location = locTok.getErrorLocation(modul.filePath());
285     auto msg = cc.diag.formatMsg(mid, _arguments, _argptr);
286     cc.diag ~= new SemanticError(location, msg);
287   }
288 
289   /// Reports the error that the module was not found.
290   /// Params:
291   ///   modulePath = File path to the module.
292   ///   loc = Optional source location (from an import statement.)
293   void errorModuleNotFound(cstring modulePath, Location loc = null)
294   {
295     if(!loc) loc = new Location(modulePath, 0);
296     auto msg = cc.diag.formatMsg(MID.CouldntLoadModule, modulePath);
297     cc.diag ~= new LexerError(loc, msg);
298   }
299 }