1 /**
2  * This file is part of DCD, a development tool for the D programming language.
3  * Copyright (C) 2014 Brian Schott
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 module dsymbol.modulecache;
20 
21 import containers.dynamicarray;
22 import containers.hashset;
23 import containers.ttree;
24 import containers.unrolledlist;
25 import dsymbol.conversion;
26 import dsymbol.conversion.first;
27 import dsymbol.conversion.second;
28 import dsymbol.cache_entry;
29 import dsymbol.scope_;
30 import dsymbol.semantic;
31 import dsymbol.symbol;
32 import dsymbol.string_interning;
33 import dsymbol.deferred;
34 import std.algorithm;
35 import stdx.allocator;
36 import stdx.allocator.building_blocks.allocator_list;
37 import stdx.allocator.building_blocks.region;
38 import stdx.allocator.building_blocks.null_allocator;
39 import stdx.allocator.mallocator;
40 import std.conv;
41 import dparse.ast;
42 import std.datetime;
43 import dparse.lexer;
44 import dparse.parser;
45 import std.experimental.logger;
46 import std.file;
47 import std.experimental.lexer;
48 import std.path;
49 
50 alias ASTAllocator = CAllocatorImpl!(AllocatorList!(
51 	n => Region!Mallocator(1024 * 128), Mallocator));
52 
53 /**
54  * Returns: true if a file exists at the given path.
55  */
56 bool existanceCheck(A)(A path)
57 {
58 	if (path.exists())
59 		return true;
60 	warning("Cannot cache modules in ", path, " because it does not exist");
61 	return false;
62 }
63 
64 /**
65  * Caches pre-parsed module information.
66  */
67 struct ModuleCache
68 {
69 	/// No copying.
70 	@disable this(this);
71 
72 	@disable this();
73 
74 	this(IAllocator symbolAllocator)
75 	{
76 		this.symbolAllocator = symbolAllocator;
77 	}
78 
79 	~this()
80 	{
81 		foreach (entry; ModuleCache.cache[])
82 			Mallocator.instance.dispose(entry);
83 		foreach (symbol; deferredSymbols[])
84 			Mallocator.instance.dispose(symbol);
85 
86 		// TODO: This call to deallocateAll is a workaround for issues of
87 		// CAllocatorImpl and GCAllocator not interacting well.
88 		symbolAllocator.deallocateAll();
89 	}
90 
91 	/**
92 	 * Adds the given path to the list of directories checked for imports.
93 	 * Performs duplicate checking, so multiple instances of the same path will
94 	 * not be present.
95 	 */
96 	void addImportPaths(const string[] paths)
97 	{
98 		import dsymbol.string_interning : internString;
99 		import std.path : baseName;
100 		import std.array : array;
101 
102 		auto newPaths = paths
103 			.map!(a => absolutePath(expandTilde(a)))
104 			.filter!(a => existanceCheck(a) && !importPaths[].canFind(a))
105 			.map!(internString)
106 			.array;
107 		importPaths.insert(newPaths);
108 
109 		foreach (path; newPaths[])
110 		{
111 			if (path.isFile)
112 			{
113 				if (path.baseName.startsWith(".#"))
114 					continue;
115 				cacheModule(path);
116 			}
117 			else
118 			{
119 				void scanFrom(const string root)
120 				{
121 					if (exists(buildPath(root, ".no-dcd")))
122 						return;
123 
124 					try foreach (f; dirEntries(root, SpanMode.shallow))
125 					{
126 						if (f.name.isFile)
127 						{
128 							if (!f.name.extension.among(".d", ".di") || f.name.baseName.startsWith(".#"))
129 								continue;
130 							cacheModule(f.name);
131 						}
132 						else scanFrom(f.name);
133 					}
134 					catch(FileException) {}
135 				}
136 				scanFrom(path);
137 			}
138 		}
139 	}
140 
141 	/// TODO: Implement
142 	void clear()
143 	{
144 		info("ModuleCache.clear is not yet implemented.");
145 	}
146 
147 	/**
148 	 * Caches the module at the given location
149 	 */
150 	DSymbol* cacheModule(string location)
151 	{
152 		import dsymbol.string_interning : internString;
153 		import std.stdio : File;
154 		import std.typecons : scoped;
155 
156 		assert (location !is null);
157 
158 		istring cachedLocation = internString(location);
159 
160 
161 		if (!needsReparsing(cachedLocation))
162 			return getModuleSymbol(cachedLocation);
163 
164 		recursionGuard.insert(cachedLocation);
165 
166 		File f = File(cachedLocation);
167 		immutable fileSize = cast(size_t) f.size;
168 		if (fileSize == 0)
169 			return null;
170 
171 		const(Token)[] tokens;
172 		auto parseStringCache = StringCache(StringCache.defaultBucketCount);
173 		{
174 			ubyte[] source = cast(ubyte[]) Mallocator.instance.allocate(fileSize);
175 			scope (exit) Mallocator.instance.deallocate(source);
176 			f.rawRead(source);
177 			LexerConfig config;
178 			config.fileName = cachedLocation;
179 
180 			// The first three bytes are sliced off here if the file starts with a
181 			// Unicode byte order mark. The lexer/parser don't handle them.
182 			tokens = getTokensForParser(
183 				(source.length >= 3 && source[0 .. 3] == "\xef\xbb\xbf"c)
184 				? source[3 .. $] : source,
185 				config, &parseStringCache);
186 		}
187 
188 		CacheEntry* newEntry = Mallocator.instance.make!CacheEntry();
189 
190 		auto semanticAllocator = scoped!(ASTAllocator);
191 		import dparse.rollback_allocator:RollbackAllocator;
192 		RollbackAllocator parseAllocator;
193 		Module m = parseModuleSimple(tokens[], cachedLocation, &parseAllocator);
194 
195 		assert (symbolAllocator);
196 		auto first = scoped!FirstPass(m, cachedLocation, symbolAllocator,
197 			semanticAllocator, false, &this, newEntry);
198 		first.run();
199 
200 		secondPass(first.rootSymbol, first.moduleScope, this);
201 
202 		typeid(Scope).destroy(first.moduleScope);
203 		symbolsAllocated += first.symbolsAllocated;
204 
205 		SysTime access;
206 		SysTime modification;
207 		getTimes(cachedLocation.data, access, modification);
208 
209 		newEntry.symbol = first.rootSymbol.acSymbol;
210 		newEntry.modificationTime = modification;
211 		newEntry.path = cachedLocation;
212 
213 		CacheEntry* oldEntry = getEntryFor(cachedLocation);
214 		if (oldEntry !is null)
215 		{
216 			// Generate update mapping from the old symbol to the new one
217 			UpdatePairCollection updatePairs;
218 			generateUpdatePairs(oldEntry.symbol, newEntry.symbol, updatePairs);
219 
220 			// Apply updates to all symbols in modules that depend on this one
221 			cache[].filter!(a => a.dependencies.contains(cachedLocation)).each!(
222 				upstream => upstream.symbol.updateTypes(updatePairs));
223 
224 			// Remove the old symbol.
225 			cache.remove(oldEntry, entry => Mallocator.instance.dispose(entry));
226 		}
227 
228 		cache.insert(newEntry);
229 		recursionGuard.remove(cachedLocation);
230 
231 		resolveDeferredTypes(cachedLocation);
232 
233 		typeid(SemanticSymbol).destroy(first.rootSymbol);
234 
235 		return newEntry.symbol;
236 	}
237 
238 	/**
239 	 * Resolves types for deferred symbols
240 	 */
241 	void resolveDeferredTypes(istring location)
242 	{
243 		UnrolledList!(DeferredSymbol*) temp;
244 		temp.insert(deferredSymbols[]);
245 		deferredSymbols.clear();
246 		foreach (deferred; temp[])
247 		{
248 			if (!deferred.imports.empty && !deferred.dependsOn(location))
249 			{
250 				deferredSymbols.insert(deferred);
251 				continue;
252 			}
253 			assert(deferred.symbol.type is null);
254 			if (deferred.symbol.kind == CompletionKind.importSymbol)
255 			{
256 				resolveImport(deferred.symbol, deferred.typeLookups, this);
257 			}
258 			else if (!deferred.typeLookups.empty)
259 			{
260 				// TODO: Is .front the right thing to do here?
261 				resolveTypeFromType(deferred.symbol, deferred.typeLookups.front, null,
262 					this, &deferred.imports);
263 			}
264 			Mallocator.instance.dispose(deferred);
265 		}
266 	}
267 
268 	/**
269 	 * Params:
270 	 *     moduleName = the name of the module in "a/b/c" form
271 	 * Returns:
272 	 *     The symbols defined in the given module, or null if the module is
273 	 *     not cached yet.
274 	 */
275 	DSymbol* getModuleSymbol(istring location)
276 	{
277 		auto existing = getEntryFor(location);
278 		return existing is null ? null : existing.symbol;
279 	}
280 
281 	/**
282 	 * Params:
283 	 *     moduleName = the name of the module being imported, in "a/b/c" style
284 	 * Returns:
285 	 *     The absolute path to the file that contains the module, or null if
286 	 *     not found.
287 	 */
288 	istring resolveImportLocation(string moduleName)
289 	{
290 		assert(moduleName !is null, "module name is null");
291 		if (isRooted(moduleName))
292 			return internString(moduleName);
293 		string[] alternatives;
294 		foreach (path; importPaths[])
295 		{
296 			if (path.isFile)
297 			{
298 				if (path.stripExtension.endsWith(moduleName))
299 					alternatives ~= path;
300 			}
301 			else
302 			{
303 				string dotDi = buildPath(path, moduleName) ~ ".di";
304 				string dotD = dotDi[0 .. $ - 1];
305 				string withoutSuffix = dotDi[0 .. $ - 3];
306 				if (exists(dotD) && isFile(dotD))
307 					alternatives = dotD ~ alternatives;
308 				else if (exists(dotDi) && isFile(dotDi))
309 					alternatives ~= dotDi;
310 				else if (exists(withoutSuffix) && isDir(withoutSuffix))
311 				{
312 					string packagePath = buildPath(withoutSuffix, "package.di");
313 					if (exists(packagePath) && isFile(packagePath))
314 					{
315 						alternatives ~= packagePath;
316 						continue;
317 					}
318 					if (exists(packagePath[0 .. $ - 1]) && isFile(packagePath[0 .. $ - 1]))
319 						alternatives ~= packagePath[0 .. $ - 1];
320 				}
321 			}
322 		}
323 		return alternatives.length > 0 ? internString(alternatives[0]) : istring(null);
324 	}
325 
326 	auto getImportPaths() const
327 	{
328 		return importPaths[];
329 	}
330 
331 	auto getAllSymbols() const
332 	{
333 		return cache[];
334 	}
335 
336 	IAllocator symbolAllocator;
337 
338 	UnrolledList!(DeferredSymbol*) deferredSymbols;
339 
340 	/// Count of autocomplete symbols that have been allocated
341 	uint symbolsAllocated;
342 
343 private:
344 
345 	CacheEntry* getEntryFor(istring cachedLocation)
346 	{
347 		CacheEntry dummy;
348 		dummy.path = cachedLocation;
349 		auto r = cache.equalRange(&dummy);
350 		return r.empty ? null : r.front;
351 	}
352 
353 	/**
354 	 * Params:
355 	 *     mod = the path to the module
356 	 * Returns:
357 	 *     true  if the module needs to be reparsed, false otherwise
358 	 */
359 	bool needsReparsing(istring mod)
360 	{
361 		if (recursionGuard.contains(mod))
362 			return false;
363 		if (!exists(mod.data))
364 			return true;
365 		CacheEntry e;
366 		e.path = mod;
367 		auto r = cache.equalRange(&e);
368 		if (r.empty)
369 			return true;
370 		SysTime access;
371 		SysTime modification;
372 		getTimes(mod.data, access, modification);
373 		return r.front.modificationTime != modification;
374 	}
375 
376 	// Mapping of file paths to their cached symbols.
377 	TTree!(CacheEntry*) cache;
378 
379 	HashSet!string recursionGuard;
380 
381 	// Listing of paths to check for imports
382 	UnrolledList!string importPaths;
383 }