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 std.experimental.allocator;
36 import std.experimental.allocator.building_blocks.allocator_list;
37 import std.experimental.allocator.building_blocks.region;
38 import std.experimental.allocator.building_blocks.null_allocator;
39 import std.experimental.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 					try foreach (f; dirEntries(root, SpanMode.shallow))
122 					{
123 						if (f.name.isFile)
124 						{
125 							if (!f.name.extension.among(".d", ".di") || f.name.baseName.startsWith(".#"))
126 								continue;
127 							cacheModule(f.name);
128 						}
129 						else scanFrom(f.name);
130 					}
131 					catch(FileException) {}
132 				}
133 				scanFrom(path);
134 			}
135 		}
136 	}
137 
138 	/// TODO: Implement
139 	void clear()
140 	{
141 		info("ModuleCache.clear is not yet implemented.");
142 	}
143 
144 	/**
145 	 * Caches the module at the given location
146 	 */
147 	DSymbol* cacheModule(string location)
148 	{
149 		import dsymbol.string_interning : internString;
150 		import std.stdio : File;
151 		import std.typecons : scoped;
152 
153 		assert (location !is null);
154 
155 		istring cachedLocation = internString(location);
156 
157 
158 		if (!needsReparsing(cachedLocation))
159 			return getModuleSymbol(cachedLocation);
160 
161 		recursionGuard.insert(cachedLocation);
162 
163 		File f = File(cachedLocation);
164 		immutable fileSize = cast(size_t) f.size;
165 		if (fileSize == 0)
166 			return null;
167 
168 		const(Token)[] tokens;
169 		auto parseStringCache = StringCache(StringCache.defaultBucketCount);
170 		{
171 			ubyte[] source = cast(ubyte[]) Mallocator.instance.allocate(fileSize);
172 			scope (exit) Mallocator.instance.deallocate(source);
173 			f.rawRead(source);
174 			LexerConfig config;
175 			config.fileName = cachedLocation;
176 
177 			// The first three bytes are sliced off here if the file starts with a
178 			// Unicode byte order mark. The lexer/parser don't handle them.
179 			tokens = getTokensForParser(
180 				(source.length >= 3 && source[0 .. 3] == "\xef\xbb\xbf"c)
181 				? source[3 .. $] : source,
182 				config, &parseStringCache);
183 		}
184 
185 		CacheEntry* newEntry = Mallocator.instance.make!CacheEntry();
186 
187 		auto semanticAllocator = scoped!(ASTAllocator);
188 		import dparse.rollback_allocator:RollbackAllocator;
189 		RollbackAllocator parseAllocator;
190 		Module m = parseModuleSimple(tokens[], cachedLocation, &parseAllocator);
191 
192 		assert (symbolAllocator);
193 		auto first = scoped!FirstPass(m, cachedLocation, symbolAllocator,
194 			semanticAllocator, false, &this, newEntry);
195 		first.run();
196 
197 		secondPass(first.rootSymbol, first.moduleScope, this);
198 
199 		typeid(Scope).destroy(first.moduleScope);
200 		symbolsAllocated += first.symbolsAllocated;
201 
202 		SysTime access;
203 		SysTime modification;
204 		getTimes(cachedLocation.data, access, modification);
205 
206 		newEntry.symbol = first.rootSymbol.acSymbol;
207 		newEntry.modificationTime = modification;
208 		newEntry.path = cachedLocation;
209 
210 		CacheEntry* oldEntry = getEntryFor(cachedLocation);
211 		if (oldEntry !is null)
212 		{
213 			// Generate update mapping from the old symbol to the new one
214 			UpdatePairCollection updatePairs;
215 			generateUpdatePairs(oldEntry.symbol, newEntry.symbol, updatePairs);
216 
217 			// Apply updates to all symbols in modules that depend on this one
218 			cache[].filter!(a => a.dependencies.contains(cachedLocation)).each!(
219 				upstream => upstream.symbol.updateTypes(updatePairs));
220 
221 			// Remove the old symbol.
222 			cache.remove(oldEntry, entry => Mallocator.instance.dispose(entry));
223 		}
224 
225 		cache.insert(newEntry);
226 		recursionGuard.remove(cachedLocation);
227 
228 		resolveDeferredTypes(cachedLocation);
229 
230 		typeid(SemanticSymbol).destroy(first.rootSymbol);
231 
232 		return newEntry.symbol;
233 	}
234 
235 	/**
236 	 * Resolves types for deferred symbols
237 	 */
238 	void resolveDeferredTypes(istring location)
239 	{
240 		UnrolledList!(DeferredSymbol*) temp;
241 		temp.insert(deferredSymbols[]);
242 		deferredSymbols.clear();
243 		foreach (deferred; temp[])
244 		{
245 			if (!deferred.imports.empty && !deferred.dependsOn(location))
246 			{
247 				deferredSymbols.insert(deferred);
248 				continue;
249 			}
250 			assert(deferred.symbol.type is null);
251 			if (deferred.symbol.kind == CompletionKind.importSymbol)
252 			{
253 				resolveImport(deferred.symbol, deferred.typeLookups, this);
254 			}
255 			else if (!deferred.typeLookups.empty)
256 			{
257 				// TODO: Is .front the right thing to do here?
258 				resolveTypeFromType(deferred.symbol, deferred.typeLookups.front, null,
259 					this, &deferred.imports);
260 			}
261 			Mallocator.instance.dispose(deferred);
262 		}
263 	}
264 
265 	/**
266 	 * Params:
267 	 *     moduleName = the name of the module in "a/b/c" form
268 	 * Returns:
269 	 *     The symbols defined in the given module, or null if the module is
270 	 *     not cached yet.
271 	 */
272 	DSymbol* getModuleSymbol(istring location)
273 	{
274 		auto existing = getEntryFor(location);
275 		return existing is null ? null : existing.symbol;
276 	}
277 
278 	/**
279 	 * Params:
280 	 *     moduleName = the name of the module being imported, in "a/b/c" style
281 	 * Returns:
282 	 *     The absolute path to the file that contains the module, or null if
283 	 *     not found.
284 	 */
285 	istring resolveImportLocation(string moduleName)
286 	{
287 		assert(moduleName !is null, "module name is null");
288 		if (isRooted(moduleName))
289 			return internString(moduleName);
290 		string[] alternatives;
291 		foreach (path; importPaths[])
292 		{
293 			if (path.isFile)
294 			{
295 				if (path.stripExtension.endsWith(moduleName))
296 					alternatives ~= path;
297 			}
298 			else
299 			{
300 				string dotDi = buildPath(path, moduleName) ~ ".di";
301 				string dotD = dotDi[0 .. $ - 1];
302 				string withoutSuffix = dotDi[0 .. $ - 3];
303 				if (exists(dotD) && isFile(dotD))
304 					alternatives = dotD ~ alternatives;
305 				else if (exists(dotDi) && isFile(dotDi))
306 					alternatives ~= dotDi;
307 				else if (exists(withoutSuffix) && isDir(withoutSuffix))
308 				{
309 					string packagePath = buildPath(withoutSuffix, "package.di");
310 					if (exists(packagePath) && isFile(packagePath))
311 					{
312 						alternatives ~= packagePath;
313 						continue;
314 					}
315 					if (exists(packagePath[0 .. $ - 1]) && isFile(packagePath[0 .. $ - 1]))
316 						alternatives ~= packagePath[0 .. $ - 1];
317 				}
318 			}
319 		}
320 		return alternatives.length > 0 ? internString(alternatives[0]) : istring(null);
321 	}
322 
323 	auto getImportPaths() const
324 	{
325 		return importPaths[];
326 	}
327 
328 	auto getAllSymbols() const
329 	{
330 		return cache[];
331 	}
332 
333 	IAllocator symbolAllocator;
334 
335 	UnrolledList!(DeferredSymbol*) deferredSymbols;
336 
337 	/// Count of autocomplete symbols that have been allocated
338 	uint symbolsAllocated;
339 
340 private:
341 
342 	CacheEntry* getEntryFor(istring cachedLocation)
343 	{
344 		CacheEntry dummy;
345 		dummy.path = cachedLocation;
346 		auto r = cache.equalRange(&dummy);
347 		return r.empty ? null : r.front;
348 	}
349 
350 	/**
351 	 * Params:
352 	 *     mod = the path to the module
353 	 * Returns:
354 	 *     true  if the module needs to be reparsed, false otherwise
355 	 */
356 	bool needsReparsing(istring mod)
357 	{
358 		if (recursionGuard.contains(mod))
359 			return false;
360 		if (!exists(mod.data))
361 			return true;
362 		CacheEntry e;
363 		e.path = mod;
364 		auto r = cache.equalRange(&e);
365 		if (r.empty)
366 			return true;
367 		SysTime access;
368 		SysTime modification;
369 		getTimes(mod.data, access, modification);
370 		return r.front.modificationTime != modification;
371 	}
372 
373 	// Mapping of file paths to their cached symbols.
374 	TTree!(CacheEntry*) cache;
375 
376 	HashSet!string recursionGuard;
377 
378 	// Listing of paths to check for imports
379 	UnrolledList!string importPaths;
380 }