1 /// DustMite, a D test case minimization tool
2 /// Written by Vladimir Panteleev <vladimir@thecybershadow.net>
3 /// Released into the Public Domain
4 
5 module dustmite;
6 
7 import std.algorithm;
8 import std.array;
9 import std.ascii;
10 import std.conv;
11 import std.datetime;
12 import std.datetime.stopwatch : StopWatch;
13 import std.exception;
14 import std.file;
15 import std.getopt;
16 import std.path;
17 import std.parallelism : totalCPUs;
18 import std.process;
19 import std.random;
20 import std.regex;
21 import std.stdio;
22 import std..string;
23 
24 import splitter;
25 
26 alias Splitter = splitter.Splitter;
27 
28 // Issue 314 workarounds
29 alias std..string.join join;
30 alias std..string.startsWith startsWith;
31 
32 string dir, resultDir, tester, globalCache;
33 string dirSuffix(string suffix) { return (dir.absolutePath().buildNormalizedPath() ~ "." ~ suffix).relativePath(); }
34 
35 size_t maxBreadth;
36 Entity root;
37 size_t origDescendants;
38 int tests; bool foundAnything;
39 bool noSave, trace, noRedirect;
40 string strategy = "inbreadth";
41 
42 struct Times { StopWatch total, load, testSave, resultSave, test, clean, cacheHash, globalCache, misc; }
43 Times times;
44 static this() { times.total.start(); times.misc.start(); }
45 void measure(string what)(void delegate() p)
46 {
47 	times.misc.stop(); mixin("times."~what~".start();");
48 	p();
49 	mixin("times."~what~".stop();"); times.misc.start();
50 }
51 
52 struct Reduction
53 {
54 	enum Type { None, Remove, Unwrap, Concat, ReplaceWord }
55 	Type type;
56 
57 	// Remove / Unwrap / Concat
58 	size_t[] address;
59 	Entity target;
60 
61 	// ReplaceWord
62 	string from, to;
63 	size_t index, total;
64 
65 	string toString()
66 	{
67 		string name = .to!string(type);
68 
69 		final switch (type)
70 		{
71 			case Reduction.Type.None:
72 				return name;
73 			case Reduction.Type.ReplaceWord:
74 				return format(`%s [%d/%d: %s -> %s]`, name, index+1, total, from, to);
75 			case Reduction.Type.Remove:
76 			case Reduction.Type.Unwrap:
77 			case Reduction.Type.Concat:
78 				string[] segments = new string[address.length];
79 				Entity e = root;
80 				size_t progress;
81 				bool binary = maxBreadth == 2;
82 				foreach (i, a; address)
83 				{
84 					segments[i] = binary ? text(a) : format("%d/%d", e.children.length-a, e.children.length);
85 					foreach (c; e.children[0..a])
86 						progress += c.descendants;
87 					progress++; // account for this node
88 					e = e.children[a];
89 				}
90 				progress += e.descendants;
91 				auto progressPM = (origDescendants-progress) * 1000 / origDescendants; // per-mille
92 				return format("[%2d.%d%%] %s [%s]", progressPM/10, progressPM%10, name, segments.join(binary ? "" : ", "));
93 		}
94 	}
95 }
96 
97 auto nullReduction = Reduction(Reduction.Type.None);
98 
99 int main(string[] args)
100 {
101 	bool force, dump, dumpHtml, showTimes, stripComments, obfuscate, keepLength, showHelp, noOptimize;
102 	string coverageDir;
103 	string[] reduceOnly, noRemoveStr, splitRules;
104 	uint lookaheadCount;
105 
106 	args = args.filter!(
107 		(arg)
108 		{
109 			if (arg.startsWith("-j"))
110 			{
111 				arg = arg[2..$];
112 				lookaheadCount = arg.length ? arg.to!uint : totalCPUs;
113 				return false;
114 			}
115 			return true;
116 		}).array();
117 
118 	getopt(args,
119 		"force", &force,
120 		"reduceonly|reduce-only", &reduceOnly,
121 		"noremove|no-remove", &noRemoveStr,
122 		"strip-comments", &stripComments,
123 		"coverage", &coverageDir,
124 		"obfuscate", &obfuscate,
125 		"keep-length", &keepLength,
126 		"strategy", &strategy,
127 		"split", &splitRules,
128 		"dump", &dump,
129 		"dump-html", &dumpHtml,
130 		"times", &showTimes,
131 		"noredirect|no-redirect", &noRedirect,
132 		"cache", &globalCache, // for research
133 		"trace", &trace, // for debugging
134 		"nosave|no-save", &noSave, // for research
135 		"nooptimize|no-optimize", &noOptimize, // for research
136 		"h|help", &showHelp,
137 	);
138 
139 	if (showHelp || args.length == 1 || args.length>3)
140 	{
141 		stderr.writef(q"EOS
142 Usage: %s [OPTION]... PATH TESTER
143 PATH should be a directory containing a clean copy of the file-set to reduce.
144 A file path can also be specified. NAME.EXT will be treated like NAME/NAME.EXT.
145 TESTER should be a shell command which returns 0 for a correct reduction,
146 and anything else otherwise.
147 Supported options:
148   --force            Force reduction of unusual files
149   --reduce-only MASK Only reduce paths glob-matching MASK
150                        (may be used multiple times)
151   --no-remove REGEXP Do not reduce blocks containing REGEXP
152                        (may be used multiple times)
153   --strip-comments   Attempt to remove comments from source code.
154   --coverage DIR     Load .lst files corresponding to source files from DIR
155   --obfuscate        Instead of reducing, obfuscate the input by replacing
156                        words with random substitutions
157   --keep-length      Preserve word length when obfuscating
158   --split MASK:MODE  Parse and reduce files specified by MASK using the given
159                        splitter. Can be repeated. MODE must be one of:
160                        %-(%s, %)
161   --no-redirect      Don't redirect stdout/stderr streams of test command.
162   -j[N]              Use N look-ahead processes (%d by default)
163 EOS", args[0], splitterNames, totalCPUs);
164 
165 		if (!showHelp)
166 		{
167 			stderr.write(q"EOS
168   --help             Show this message and some less interesting options
169 EOS");
170 		}
171 		else
172 		{
173 			stderr.write(q"EOS
174   --help             Show this message
175 Less interesting options:
176   --strategy STRAT   Set strategy (careful/lookback/pingpong/indepth/inbreadth)
177   --dump             Dump parsed tree to DIR.dump file
178   --dump-html        Dump parsed tree to DIR.html file
179   --times            Display verbose spent time breakdown
180   --cache DIR        Use DIR as persistent disk cache
181                        (in addition to memory cache)
182   --trace            Save all attempted reductions to DIR.trace
183   --no-save          Disable saving in-progress results
184   --no-optimize      Disable tree optimization step
185                        (may be useful with --dump)
186 EOS");
187 		}
188 		stderr.write(q"EOS
189 
190 Full documentation can be found on the GitHub wiki:
191   https://github.com/CyberShadow/DustMite/wiki
192 EOS");
193 		return showHelp ? 0 : 64; // EX_USAGE
194 	}
195 
196 	enforce(!(stripComments && coverageDir), "Sorry, --strip-comments is not compatible with --coverage");
197 
198 	dir = args[1];
199 	if (isDirSeparator(dir[$-1]))
200 		dir = dir[0..$-1];
201 
202 	if (args.length>=3)
203 		tester = args[2];
204 
205 	bool isDotName(string fn) { return fn.startsWith(".") && !(fn=="." || fn==".."); }
206 
207 	if (!force && isDir(dir))
208 		foreach (string path; dirEntries(dir, SpanMode.breadth))
209 			if (isDotName(baseName(path)) || isDotName(baseName(dirName(path))) || extension(path)==".o" || extension(path)==".obj" || extension(path)==".exe")
210 			{
211 				stderr.writefln("Suspicious file found: %s\nYou should use a clean copy of the source tree.\nIf it was your intention to include this file in the file-set to be reduced,\nre-run dustmite with the --force option.", path);
212 				return 1;
213 			}
214 
215 	ParseRule parseSplitRule(string rule)
216 	{
217 		auto p = rule.lastIndexOf(':');
218 		enforce(p > 0, "Invalid parse rule: " ~ rule);
219 		auto pattern = rule[0..p];
220 		auto splitterName = rule[p+1..$];
221 		auto splitterIndex = splitterNames.countUntil(splitterName);
222 		enforce(splitterIndex >= 0, "Unknown splitter: " ~ splitterName);
223 		return ParseRule(pattern, cast(Splitter)splitterIndex);
224 	}
225 
226 	ParseOptions parseOptions;
227 	parseOptions.stripComments = stripComments;
228 	parseOptions.mode = obfuscate ? ParseOptions.Mode.words : ParseOptions.Mode.source;
229 	parseOptions.rules = splitRules.map!parseSplitRule().array();
230 	measure!"load"({root = loadFiles(dir, parseOptions);});
231 	enforce(root.children.length, "No files in specified directory");
232 
233 	applyNoRemoveMagic();
234 	applyNoRemoveRegex(noRemoveStr, reduceOnly);
235 	applyNoRemoveDeps();
236 	if (coverageDir)
237 		loadCoverage(coverageDir);
238 	if (!obfuscate && !noOptimize)
239 		optimize(root);
240 	maxBreadth = getMaxBreadth(root);
241 	countDescendants(root);
242 	resetProgress();
243 	assignID(root);
244 
245 	if (dump)
246 		dumpSet(dirSuffix("dump"));
247 	if (dumpHtml)
248 		dumpToHtml(dirSuffix("html"));
249 
250 	if (tester is null)
251 	{
252 		writeln("No tester specified, exiting");
253 		return 0;
254 	}
255 
256 	resultDir = dirSuffix("reduced");
257 	enforce(!exists(resultDir), "Result directory already exists");
258 
259 	if (!test(nullReduction))
260 	{
261 		auto testerFile = dir.buildNormalizedPath(tester);
262 		version (Posix)
263 		{
264 			if (testerFile.exists && (testerFile.getAttributes() & octal!111) == 0)
265 				writeln("Hint: test program seems to be a non-executable file, try: chmod +x " ~ testerFile.escapeShellFileName());
266 		}
267 		if (!testerFile.exists && tester.exists)
268 			writeln("Hint: test program path should be relative to the source directory, try " ~
269 				tester.absolutePath.relativePath(dir.absolutePath).escapeShellFileName() ~
270 				" instead of " ~ tester.escapeShellFileName());
271 		throw new Exception("Initial test fails" ~ (noRedirect ? "" : " (try using --no-redirect for details)"));
272 	}
273 
274 	lookaheadProcesses = new Lookahead[lookaheadCount];
275 
276 	foundAnything = false;
277 	if (obfuscate)
278 		.obfuscate(keepLength);
279 	else
280 		reduce();
281 
282 	auto duration = times.total.peek();
283 	duration = dur!"msecs"(duration.total!"msecs"); // truncate anything below ms, users aren't interested in that
284 	if (foundAnything)
285 	{
286 		if (root.children.length)
287 		{
288 			if (noSave)
289 				measure!"resultSave"({safeSave(resultDir);});
290 			writefln("Done in %s tests and %s; reduced version is in %s", tests, duration, resultDir);
291 		}
292 		else
293 			writefln("Done in %s tests and %s; reduced to empty set", tests, duration);
294 	}
295 	else
296 		writefln("Done in %s tests and %s; no reductions found", tests, duration);
297 
298 	if (showTimes)
299 		foreach (i, t; times.tupleof)
300 			writefln("%s: %s", times.tupleof[i].stringof, times.tupleof[i].peek());
301 
302 	return 0;
303 }
304 
305 size_t getMaxBreadth(Entity e)
306 {
307 	size_t breadth = e.children.length;
308 	foreach (child; e.children)
309 	{
310 		auto childBreadth = getMaxBreadth(child);
311 		if (breadth < childBreadth)
312 			breadth = childBreadth;
313 	}
314 	return breadth;
315 }
316 
317 size_t countDescendants(Entity e)
318 {
319 	size_t n = 1;
320 	foreach (c; e.children)
321 		n += countDescendants(c);
322 	return e.descendants = n;
323 }
324 
325 size_t checkDescendants(Entity e)
326 {
327 	size_t n = 1;
328 	foreach (c; e.children)
329 		n += checkDescendants(c);
330 	assert(e.descendants == n, "Wrong descendant count: expected %d, found %d".format(e.descendants, n));
331 	return n;
332 }
333 
334 size_t countFiles(Entity e)
335 {
336 	if (e.isFile)
337 		return 1;
338 	else
339 	{
340 		size_t n = 0;
341 		foreach (c; e.children)
342 			n += countFiles(c);
343 		return n;
344 	}
345 }
346 
347 
348 struct ReductionIterator
349 {
350 	Strategy strategy;
351 
352 	bool done = false;
353 	bool concatPerformed;
354 
355 	Reduction.Type type = Reduction.Type.None;
356 	Entity e;
357 
358 	this(Strategy strategy)
359 	{
360 		this.strategy = strategy;
361 		next(false);
362 
363 		if (countFiles(root) < 2)
364 			concatPerformed = true;
365 	}
366 
367 	this(this)
368 	{
369 		strategy = strategy.dup;
370 	}
371 
372 	@property Reduction front() { return Reduction(type, strategy.front, e); }
373 
374 	void next(bool success)
375 	{
376 		while (true)
377 		{
378 			final switch (type)
379 			{
380 				case Reduction.Type.None:
381 					if (strategy.done)
382 					{
383 						done = true;
384 						return;
385 					}
386 
387 					e = entityAt(strategy.front);
388 
389 					if (e.noRemove)
390 					{
391 						strategy.next(false);
392 						continue;
393 					}
394 
395 					if (e is root && !root.children.length)
396 					{
397 						strategy.next(false);
398 						continue;
399 					}
400 
401 					// Try next reduction type
402 					type = Reduction.Type.Remove;
403 					return;
404 
405 				case Reduction.Type.Remove:
406 					if (success)
407 					{
408 						// Next node
409 						type = Reduction.Type.None;
410 						strategy.next(true);
411 						continue;
412 					}
413 
414 					// Try next reduction type
415 					type = Reduction.Type.Unwrap;
416 
417 					if (e.head.length && e.tail.length)
418 						return; // Try this
419 					else
420 					{
421 						success = false; // Skip
422 						continue;
423 					}
424 
425 				case Reduction.Type.Unwrap:
426 					if (success)
427 					{
428 						// Next node
429 						type = Reduction.Type.None;
430 						strategy.next(true);
431 						continue;
432 					}
433 
434 					// Try next reduction type
435 					type = Reduction.Type.Concat;
436 
437 					if (e.isFile && !concatPerformed)
438 						return; // Try this
439 					else
440 					{
441 						success = false; // Skip
442 						continue;
443 					}
444 
445 				case Reduction.Type.Concat:
446 					if (success)
447 						concatPerformed = true;
448 
449 					// Next node
450 					type = Reduction.Type.None;
451 					strategy.next(success);
452 					continue;
453 
454 				case Reduction.Type.ReplaceWord:
455 					assert(false);
456 			}
457 		}
458 	}
459 }
460 
461 void resetProgress()
462 {
463 	origDescendants = root.descendants;
464 }
465 
466 class Strategy
467 {
468 	uint progressGeneration = 0;
469 	bool done = false;
470 
471 	void copy(Strategy result) const
472 	{
473 		result.progressGeneration = this.progressGeneration;
474 		result.done = this.done;
475 	}
476 
477 	abstract @property size_t[] front();
478 	abstract void next(bool success);
479 	int getIteration() { return -1; }
480 	int getDepth() { return -1; }
481 
482 	final Strategy dup()
483 	{
484 		auto result = cast(Strategy)this.classinfo.create();
485 		copy(result);
486 		return result;
487 	}
488 }
489 
490 class SimpleStrategy : Strategy
491 {
492 	size_t[] address;
493 
494 	override void copy(Strategy target) const
495 	{
496 		super.copy(target);
497 		auto result = cast(SimpleStrategy)target;
498 		result.address = this.address.dup;
499 	}
500 
501 	override @property size_t[] front()
502 	{
503 		assert(!done, "Done");
504 		return address;
505 	}
506 
507 	override void next(bool success)
508 	{
509 		assert(!done, "Done");
510 	}
511 }
512 
513 class IterativeStrategy : SimpleStrategy
514 {
515 	int iteration = 0;
516 	bool iterationChanged;
517 
518 	override int getIteration() { return iteration; }
519 
520 	override void copy(Strategy target) const
521 	{
522 		super.copy(target);
523 		auto result = cast(IterativeStrategy)target;
524 		result.iteration = this.iteration;
525 		result.iterationChanged = this.iterationChanged;
526 	}
527 
528 	override void next(bool success)
529 	{
530 		super.next(success);
531 		iterationChanged |= success;
532 	}
533 
534 	void nextIteration()
535 	{
536 		assert(iterationChanged, "Starting new iteration after no changes");
537 		iteration++;
538 		iterationChanged = false;
539 		address = null;
540 		progressGeneration++;
541 	}
542 }
543 
544 /// Find the first address at the depth of address.length,
545 /// and populate address[] accordingly.
546 /// Return false if no address at that level could be found.
547 bool findAddressAtLevel(size_t[] address, Entity root)
548 {
549 	if (!address.length)
550 		return true;
551 	foreach_reverse (i, child; root.children)
552 	{
553 		if (findAddressAtLevel(address[1..$], child))
554 		{
555 			address[0] = i;
556 			return true;
557 		}
558 	}
559 	return false;
560 }
561 
562 /// Find the next address at the depth of address.length,
563 /// and update address[] accordingly.
564 /// Return false if no more addresses at that level could be found.
565 bool nextAddressInLevel(size_t[] address, lazy Entity root)
566 {
567 	if (!address.length)
568 		return false;
569 	if (nextAddressInLevel(address[1..$], root.children[address[0]]))
570 		return true;
571 	if (!address[0])
572 		return false;
573 
574 	foreach_reverse (i; 0..address[0])
575 	{
576 		if (findAddressAtLevel(address[1..$], root.children[i]))
577 		{
578 			address[0] = i;
579 			return true;
580 		}
581 	}
582 	return false;
583 }
584 
585 /// Find the next address, starting from the given one
586 /// (going depth-first). Update address accordingly.
587 /// If descend is false, then skip addresses under the given one.
588 /// Return false if no more addresses could be found.
589 bool nextAddress(ref size_t[] address, lazy Entity root, bool descend)
590 {
591 	if (!address.length)
592 	{
593 		if (descend && root.children.length)
594 		{
595 			address ~= [root.children.length-1];
596 			return true;
597 		}
598 		return false;
599 	}
600 
601 	auto cdr = address[1..$];
602 	if (nextAddress(cdr, root.children[address[0]], descend))
603 	{
604 		address = address[0] ~ cdr;
605 		return true;
606 	}
607 
608 	if (address[0])
609 	{
610 		address = [address[0] - 1];
611 		return true;
612 	}
613 
614 	return false;
615 }
616 
617 void validateAddress(size_t[] address, Entity root = root)
618 {
619 	if (!address.length)
620 		return;
621 	assert(address[0] < root.children.length);
622 	validateAddress(address[1..$], root.children[address[0]]);
623 }
624 
625 class LevelStrategy : IterativeStrategy
626 {
627 	bool levelChanged;
628 	bool invalid;
629 
630 	override int getDepth() { return cast(int)address.length; }
631 
632 	override void copy(Strategy target) const
633 	{
634 		super.copy(target);
635 		auto result = cast(LevelStrategy)target;
636 		result.levelChanged = this.levelChanged;
637 		result.invalid = this.invalid;
638 	}
639 
640 	override void next(bool success)
641 	{
642 		super.next(success);
643 		levelChanged |= success;
644 	}
645 
646 	override void nextIteration()
647 	{
648 		super.nextIteration();
649 		invalid = false;
650 		levelChanged = false;
651 	}
652 
653 	final bool nextInLevel()
654 	{
655 		assert(!invalid, "Choose a level!");
656 		if (nextAddressInLevel(address, root))
657 			return true;
658 		else
659 		{
660 			invalid = true;
661 			return false;
662 		}
663 	}
664 
665 	final @property size_t currentLevel() const { return address.length; }
666 
667 	final bool setLevel(size_t level)
668 	{
669 		address.length = level;
670 		if (findAddressAtLevel(address, root))
671 		{
672 			invalid = false;
673 			levelChanged = false;
674 			progressGeneration++;
675 			return true;
676 		}
677 		else
678 			return false;
679 	}
680 }
681 
682 /// Keep going deeper until we find a successful reduction.
683 /// When found, finish tests at current depth and restart from top depth (new iteration).
684 /// If we reach the bottom (depth with no nodes on it), we're done.
685 class CarefulStrategy : LevelStrategy
686 {
687 	override void next(bool success)
688 	{
689 		super.next(success);
690 
691 		if (!nextInLevel())
692 		{
693 			// End of level
694 			if (levelChanged)
695 			{
696 				nextIteration();
697 			}
698 			else
699 			if (!setLevel(currentLevel + 1))
700 			{
701 				if (iterationChanged)
702 					nextIteration();
703 				else
704 					done = true;
705 			}
706 		}
707 	}
708 }
709 
710 /// Keep going deeper until we find a successful reduction.
711 /// When found, go up a depth level.
712 /// Keep going up while we find new reductions. Repeat topmost depth level as necessary.
713 /// Once no new reductions are found at higher depths, jump to the next unvisited depth in this iteration.
714 /// If we reach the bottom (depth with no nodes on it), start a new iteration.
715 /// If we finish an iteration without finding any reductions, we're done.
716 class LookbackStrategy : LevelStrategy
717 {
718 	size_t maxLevel = 0;
719 
720 	override void copy(Strategy target) const
721 	{
722 		super.copy(target);
723 		auto result = cast(LookbackStrategy)target;
724 		result.maxLevel = this.maxLevel;
725 	}
726 
727 	override void nextIteration()
728 	{
729 		super.nextIteration();
730 		maxLevel = 0;
731 	}
732 
733 	override void next(bool success)
734 	{
735 		super.next(success);
736 
737 		if (!nextInLevel())
738 		{
739 			// End of level
740 			if (levelChanged)
741 			{
742 				setLevel(currentLevel ? currentLevel - 1 : 0);
743 			}
744 			else
745 			if (setLevel(maxLevel + 1))
746 			{
747 				maxLevel = currentLevel;
748 			}
749 			else
750 			{
751 				if (iterationChanged)
752 					nextIteration();
753 				else
754 					done = true;
755 			}
756 		}
757 	}
758 }
759 
760 /// Keep going deeper until we find a successful reduction.
761 /// When found, go up a depth level.
762 /// Keep going up while we find new reductions. Repeat topmost depth level as necessary.
763 /// Once no new reductions are found at higher depths, start going downwards again.
764 /// If we reach the bottom (depth with no nodes on it), start a new iteration.
765 /// If we finish an iteration without finding any reductions, we're done.
766 class PingPongStrategy : LevelStrategy
767 {
768 	override void next(bool success)
769 	{
770 		super.next(success);
771 
772 		if (!nextInLevel())
773 		{
774 			// End of level
775 			if (levelChanged)
776 			{
777 				setLevel(currentLevel ? currentLevel - 1 : 0);
778 			}
779 			else
780 			if (!setLevel(currentLevel + 1))
781 			{
782 				if (iterationChanged)
783 					nextIteration();
784 				else
785 					done = true;
786 			}
787 		}
788 	}
789 }
790 
791 /// Keep going deeper.
792 /// If we reach the bottom (depth with no nodes on it), start a new iteration.
793 /// If we finish an iteration without finding any reductions, we're done.
794 class InBreadthStrategy : LevelStrategy
795 {
796 	override void next(bool success)
797 	{
798 		super.next(success);
799 
800 		if (!nextInLevel())
801 		{
802 			// End of level
803 			if (!setLevel(currentLevel + 1))
804 			{
805 				if (iterationChanged)
806 					nextIteration();
807 				else
808 					done = true;
809 			}
810 		}
811 	}
812 }
813 
814 /// Look at every entity in the tree.
815 /// If we can reduce this entity, continue looking at its siblings.
816 /// Otherwise, recurse and look at its children.
817 /// End an iteration once we looked at an entire tree.
818 /// If we finish an iteration without finding any reductions, we're done.
819 class InDepthStrategy : IterativeStrategy
820 {
821 	final bool nextAddress(bool descend)
822 	{
823 		return .nextAddress(address, root, descend);
824 	}
825 
826 	override void next(bool success)
827 	{
828 		super.next(success);
829 
830 		if (!nextAddress(!success))
831 		{
832 			if (iterationChanged)
833 				nextIteration();
834 			else
835 				done = true;
836 		}
837 	}
838 }
839 
840 ReductionIterator iter;
841 
842 void reduceByStrategy(Strategy strategy)
843 {
844 	int lastIteration = -1;
845 	int lastDepth = -1;
846 	int lastProgressGeneration = -1;
847 
848 	iter = ReductionIterator(strategy);
849 
850 	while (!iter.done)
851 	{
852 		if (lastIteration != strategy.getIteration())
853 		{
854 			writefln("############### ITERATION %d ################", strategy.getIteration());
855 			lastIteration = strategy.getIteration();
856 		}
857 		if (lastDepth != strategy.getDepth())
858 		{
859 			writefln("============= Depth %d =============", strategy.getDepth());
860 			lastDepth = strategy.getDepth();
861 		}
862 		if (lastProgressGeneration != strategy.progressGeneration)
863 		{
864 			resetProgress();
865 			lastProgressGeneration = strategy.progressGeneration;
866 		}
867 
868 		auto result = tryReduction(iter.front);
869 
870 		iter.next(result);
871 	}
872 }
873 
874 void reduce()
875 {
876 	switch (strategy)
877 	{
878 		case "careful":
879 			return reduceByStrategy(new CarefulStrategy());
880 		case "lookback":
881 			return reduceByStrategy(new LookbackStrategy());
882 		case "pingpong":
883 			return reduceByStrategy(new PingPongStrategy());
884 		case "indepth":
885 			return reduceByStrategy(new InDepthStrategy());
886 		case "inbreadth":
887 			return reduceByStrategy(new InBreadthStrategy());
888 		default:
889 			throw new Exception("Unknown strategy");
890 	}
891 }
892 
893 Mt19937 rng;
894 
895 void obfuscate(bool keepLength)
896 {
897 	bool[string] wordSet;
898 	string[] words; // preserve file order
899 
900 	foreach (f; root.children)
901 	{
902 		foreach (entity; parseToWords(f.filename) ~ f.children)
903 			if (entity.head.length && !isDigit(entity.head[0]))
904 				if (entity.head !in wordSet)
905 				{
906 					wordSet[entity.head] = true;
907 					words ~= entity.head;
908 				}
909 	}
910 
911 	string idgen(size_t length)
912 	{
913 		static const first = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; // use caps to avoid collisions with reserved keywords
914 		static const other = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_";
915 
916 		if (keepLength)
917 		{
918 			auto result = new char[length];
919 			foreach (i, ref c; result)
920 				c = (i==0 ? first : other)[uniform(0, cast(uint)$, rng)];
921 
922 			return assumeUnique(result);
923 		}
924 		else
925 		{
926 			static int n;
927 			int index = n++;
928 
929 			string result;
930 			result ~= first[index % $];
931 			index /= first.length;
932 
933 			while (index)
934 				result ~= other[index % $],
935 				index /= other.length;
936 
937 			return result;
938 		}
939 	}
940 
941 	auto r = Reduction(Reduction.Type.ReplaceWord);
942 	r.total = words.length;
943 	foreach (i, word; words)
944 	{
945 		r.index = i;
946 		r.from = word;
947 		int tries = 0;
948 		do
949 			r.to = idgen(word.length);
950 		while (r.to in wordSet && tries++ < 10);
951 		wordSet[r.to] = true;
952 
953 		tryReduction(r);
954 	}
955 }
956 
957 bool skipEntity(Entity e)
958 {
959 	if (e.removed)
960 		return true;
961 	foreach (dependency; e.dependencies)
962 		if (skipEntity(dependency))
963 			return true;
964 	return false;
965 }
966 
967 void dump(Entity root, ref Reduction reduction, void delegate(string) handleFile, void delegate(string) handleText)
968 {
969 	void dumpEntity(Entity e)
970 	{
971 		if (reduction.type == Reduction.Type.ReplaceWord)
972 		{
973 			if (e.isFile)
974 			{
975 				assert(e.head.length==0 && e.tail.length==0);
976 				handleFile(applyReductionToPath(e.filename, reduction));
977 				foreach (c; e.children)
978 					dumpEntity(c);
979 			}
980 			else
981 			if (e.head || e.tail)
982 			{
983 				assert(e.children.length==0);
984 				if (e.head)
985 				{
986 					if (e.head == reduction.from)
987 						handleText(reduction.to);
988 					else
989 						handleText(e.head);
990 				}
991 				handleText(e.tail);
992 			}
993 			else
994 				foreach (c; e.children)
995 					dumpEntity(c);
996 		}
997 		else
998 		if (e is reduction.target)
999 		{
1000 			final switch (reduction.type)
1001 			{
1002 			case Reduction.Type.None:
1003 			case Reduction.Type.ReplaceWord:
1004 				assert(0);
1005 			case Reduction.Type.Remove: // skip this entity
1006 				return;
1007 			case Reduction.Type.Unwrap: // skip head/tail
1008 				foreach (c; e.children)
1009 					dumpEntity(c);
1010 				break;
1011 			case Reduction.Type.Concat: // write contents of all files to this one; leave other files empty
1012 				handleFile(e.filename);
1013 
1014 				void dumpFileContent(Entity e)
1015 				{
1016 					foreach (f; e.children)
1017 						if (f.isFile)
1018 							foreach (c; f.children)
1019 								dumpEntity(c);
1020 						else
1021 							dumpFileContent(f);
1022 				}
1023 				dumpFileContent(root);
1024 				break;
1025 			}
1026 		}
1027 		else
1028 		if (skipEntity(e))
1029 			return;
1030 		else
1031 		if (e.isFile)
1032 		{
1033 			handleFile(e.filename);
1034 			if (reduction.type == Reduction.Type.Concat) // not the target - writing an empty file
1035 				return;
1036 			foreach (c; e.children)
1037 				dumpEntity(c);
1038 		}
1039 		else
1040 		{
1041 			if (e.head.length) handleText(e.head);
1042 			foreach (c; e.children)
1043 				dumpEntity(c);
1044 			if (e.tail.length) handleText(e.tail);
1045 		}
1046 	}
1047 
1048 	debug verifyNotRemoved(root);
1049 	if (reduction.type == Reduction.Type.Remove)
1050 		markRemoved(reduction.target, true); // Needed for dependencies
1051 
1052 	dumpEntity(root);
1053 
1054 	if (reduction.type == Reduction.Type.Remove)
1055 		markRemoved(reduction.target, false);
1056 	debug verifyNotRemoved(root);
1057 }
1058 
1059 void save(Reduction reduction, string savedir)
1060 {
1061 	safeMkdir(savedir);
1062 
1063 	File o;
1064 
1065 	void handleFile(string fn)
1066 	{
1067 		auto path = buildPath(savedir, fn);
1068 		if (!exists(dirName(path)))
1069 			safeMkdir(dirName(path));
1070 
1071 		if (o.isOpen)
1072 			o.close();
1073 		o.open(path, "wb");
1074 	}
1075 
1076 	dump(root, reduction, &handleFile, &o.write!string);
1077 
1078 	if (o.isOpen)
1079 		o.close();
1080 }
1081 
1082 Entity entityAt(size_t[] address)
1083 {
1084 	Entity e = root;
1085 	foreach (a; address)
1086 		e = e.children[a];
1087 	return e;
1088 }
1089 
1090 /// Try specified reduction. If it succeeds, apply it permanently and save intermediate result.
1091 bool tryReduction(Reduction r)
1092 {
1093 	if (test(r))
1094 	{
1095 		foundAnything = true;
1096 		debug
1097 			auto hashBefore = hash(r);
1098 		applyReduction(r);
1099 		debug
1100 		{
1101 			auto hashAfter = hash(nullReduction);
1102 			assert(hashBefore == hashAfter, "Reduction preview/application mismatch");
1103 		}
1104 		saveResult();
1105 		return true;
1106 	}
1107 	return false;
1108 }
1109 
1110 void verifyNotRemoved(Entity e)
1111 {
1112 	assert(!e.removed);
1113 	foreach (c; e.children)
1114 		verifyNotRemoved(c);
1115 }
1116 
1117 void markRemoved(Entity e, bool value)
1118 {
1119 	assert(e.removed == !value);
1120 	e.removed = value;
1121 	foreach (c; e.children)
1122 		markRemoved(c, value);
1123 }
1124 
1125 /// Permanently apply specified reduction to set.
1126 void applyReduction(ref Reduction r)
1127 {
1128 	final switch (r.type)
1129 	{
1130 		case Reduction.Type.None:
1131 			return;
1132 		case Reduction.Type.ReplaceWord:
1133 		{
1134 			foreach (ref f; root.children)
1135 			{
1136 				f.filename = applyReductionToPath(f.filename, r);
1137 				foreach (ref entity; f.children)
1138 					if (entity.head == r.from)
1139 						entity.head = r.to;
1140 			}
1141 			return;
1142 		}
1143 		case Reduction.Type.Remove:
1144 		{
1145 			debug verifyNotRemoved(root);
1146 
1147 			markRemoved(entityAt(r.address), true);
1148 
1149 			if (r.address.length)
1150 			{
1151 				auto casualties = entityAt(r.address).descendants;
1152 				foreach (n; 0..r.address.length)
1153 					entityAt(r.address[0..n]).descendants -= casualties;
1154 
1155 				auto p = entityAt(r.address[0..$-1]);
1156 				p.children = remove(p.children, r.address[$-1]);
1157 			}
1158 			else
1159 			{
1160 				root = new Entity();
1161 				root.descendants = 1;
1162 			}
1163 
1164 			debug verifyNotRemoved(root);
1165 			debug checkDescendants(root);
1166 			return;
1167 		}
1168 		case Reduction.Type.Unwrap:
1169 			with (entityAt(r.address))
1170 				head = tail = null;
1171 			return;
1172 		case Reduction.Type.Concat:
1173 		{
1174 			Entity[] allData;
1175 			void scan(Entity e)
1176 			{
1177 				if (e.isFile)
1178 				{
1179 					allData ~= e.children;
1180 					e.children = null;
1181 				}
1182 				else
1183 					foreach (c; e.children)
1184 						scan(c);
1185 			}
1186 
1187 			scan(root);
1188 
1189 			r.target.children = allData;
1190 			optimize(r.target);
1191 			countDescendants(root);
1192 
1193 			return;
1194 		}
1195 	}
1196 }
1197 
1198 string applyReductionToPath(string path, Reduction reduction)
1199 {
1200 	if (reduction.type == Reduction.Type.ReplaceWord)
1201 	{
1202 		Entity[] words = parseToWords(path);
1203 		string result;
1204 		foreach (i, word; words)
1205 		{
1206 			if (i > 0 && i == words.length-1 && words[i-1].tail.endsWith("."))
1207 				result ~= word.head; // skip extension
1208 			else
1209 			if (word.head == reduction.from)
1210 				result ~= reduction.to;
1211 			else
1212 				result ~= word.head;
1213 			result ~= word.tail;
1214 		}
1215 		return result;
1216 	}
1217 	return path;
1218 }
1219 
1220 void autoRetry(void delegate() fun, string operation)
1221 {
1222 	while (true)
1223 		try
1224 		{
1225 			fun();
1226 			return;
1227 		}
1228 		catch (Exception e)
1229 		{
1230 			writeln("Error while attempting to " ~ operation ~ ": " ~ e.msg);
1231 			import core.thread;
1232 			Thread.sleep(dur!"seconds"(1));
1233 			writeln("Retrying...");
1234 		}
1235 }
1236 
1237 /// Alternative way to check for file existence
1238 /// Files marked for deletion act as inexistant, but still prevent creation and appear in directory listings
1239 bool exists2(string path)
1240 {
1241 	return array(dirEntries(dirName(path), baseName(path), SpanMode.shallow)).length > 0;
1242 }
1243 
1244 void deleteAny(string path)
1245 {
1246 	if (exists(path))
1247 	{
1248 		if (isDir(path))
1249 			rmdirRecurse(path);
1250 		else
1251 			remove(path);
1252 	}
1253 	enforce(!exists(path) && !exists2(path), "Path still exists"); // Windows only marks locked directories for deletion
1254 }
1255 
1256 void safeDelete(string path) { autoRetry({deleteAny(path);}, "delete " ~ path); }
1257 void safeRename(string src, string dst) { autoRetry({rename(src, dst);}, "rename " ~ src ~ " to " ~ dst); }
1258 void safeMkdir(string path) { autoRetry({mkdirRecurse(path);}, "mkdir " ~ path); }
1259 
1260 void safeReplace(string path, void delegate(string path) creator)
1261 {
1262 	auto tmpPath = path ~ ".inprogress";
1263 	if (exists(tmpPath)) safeDelete(tmpPath);
1264 	auto oldPath = path ~ ".old";
1265 	if (exists(oldPath)) safeDelete(oldPath);
1266 
1267 	{
1268 		scope(failure) safeDelete(tmpPath);
1269 		creator(tmpPath);
1270 	}
1271 
1272 	if (exists(path)) safeRename(path, oldPath);
1273 	safeRename(tmpPath, path);
1274 	if (exists(oldPath)) safeDelete(oldPath);
1275 }
1276 
1277 
1278 void safeSave(string savedir) { safeReplace(savedir, path => save(nullReduction, path)); }
1279 
1280 void saveResult()
1281 {
1282 	if (!noSave)
1283 		measure!"resultSave"({safeSave(resultDir);});
1284 }
1285 
1286 version(HAVE_AE)
1287 {
1288 	// Use faster murmurhash from http://github.com/CyberShadow/ae
1289 	// when compiled with -version=HAVE_AE
1290 
1291 	import ae.utils.digest;
1292 	import ae.utils.textout;
1293 
1294 	alias MH3Digest128 HASH;
1295 
1296 	HASH hash(Reduction reduction)
1297 	{
1298 		static StringBuffer sb;
1299 		sb.clear();
1300 		auto writer = &sb.put!string;
1301 		dump(root, reduction, writer, writer);
1302 		return murmurHash3_128(sb.get());
1303 	}
1304 
1305 	alias digestToStringMH3 formatHash;
1306 }
1307 else
1308 {
1309 	import std.digest.md;
1310 
1311 	alias ubyte[16] HASH;
1312 
1313 	HASH hash(Reduction reduction)
1314 	{
1315 		ubyte[16] digest;
1316 		MD5 context;
1317 		context.start();
1318 		auto writer = cast(void delegate(string))&context.put;
1319 		dump(root, reduction, writer, writer);
1320 		return context.finish();
1321 	}
1322 
1323 	alias toHexString formatHash;
1324 }
1325 
1326 struct Lookahead
1327 {
1328 	Pid pid;
1329 	string testdir;
1330 	HASH digest;
1331 }
1332 Lookahead[] lookaheadProcesses;
1333 
1334 bool[HASH] lookaheadResults;
1335 
1336 bool lookaheadPredict() { return false; }
1337 
1338 version (Windows)
1339 	enum nullFileName = "nul";
1340 else
1341 	enum nullFileName = "/dev/null";
1342 
1343 bool[HASH] cache;
1344 
1345 bool test(Reduction reduction)
1346 {
1347 	write(reduction, " => "); stdout.flush();
1348 
1349 	HASH digest;
1350 	measure!"cacheHash"({ digest = hash(reduction); });
1351 
1352 	bool ramCached(lazy bool fallback)
1353 	{
1354 		auto cacheResult = digest in cache;
1355 		if (cacheResult)
1356 		{
1357 			// Note: as far as I can see, a cache hit for a positive reduction is not possible (except, perhaps, for a no-op reduction)
1358 			writeln(*cacheResult ? "Yes" : "No", " (cached)");
1359 			return *cacheResult;
1360 		}
1361 		auto result = fallback;
1362 		return cache[digest] = result;
1363 	}
1364 
1365 	bool diskCached(lazy bool fallback)
1366 	{
1367 		tests++;
1368 
1369 		if (globalCache)
1370 		{
1371 			if (!exists(globalCache)) mkdirRecurse(globalCache);
1372 			string cacheBase = absolutePath(buildPath(globalCache, formatHash(digest))) ~ "-";
1373 			bool found;
1374 
1375 			measure!"globalCache"({ found = exists(cacheBase~"0"); });
1376 			if (found)
1377 			{
1378 				writeln("No (disk cache)");
1379 				return false;
1380 			}
1381 			measure!"globalCache"({ found = exists(cacheBase~"1"); });
1382 			if (found)
1383 			{
1384 				writeln("Yes (disk cache)");
1385 				return true;
1386 			}
1387 			auto result = fallback;
1388 			measure!"globalCache"({ autoRetry({ std.file.write(cacheBase ~ (result ? "1" : "0"), ""); }, "save result to disk cache"); });
1389 			return result;
1390 		}
1391 		else
1392 			return fallback;
1393 	}
1394 
1395 	bool lookahead(lazy bool fallback)
1396 	{
1397 		if (iter.strategy)
1398 		{
1399 			// Handle existing lookahead jobs
1400 
1401 			bool reap(ref Lookahead process, int status)
1402 			{
1403 				safeDelete(process.testdir);
1404 				process.pid = null;
1405 				return lookaheadResults[process.digest] = status == 0;
1406 			}
1407 
1408 			foreach (ref process; lookaheadProcesses)
1409 			{
1410 				if (process.pid)
1411 				{
1412 					auto waitResult = process.pid.tryWait();
1413 					if (waitResult.terminated)
1414 						reap(process, waitResult.status);
1415 				}
1416 			}
1417 
1418 			// Start new lookahead jobs
1419 
1420 			auto lookaheadIter = iter;
1421 			if (!lookaheadIter.done)
1422 				lookaheadIter.next(lookaheadPredict());
1423 
1424 			foreach (ref process; lookaheadProcesses)
1425 			{
1426 				if (!process.pid && !lookaheadIter.done)
1427 				{
1428 					auto initialReduction = lookaheadIter.front;
1429 					bool first = true;
1430 
1431 					while (true)
1432 					{
1433 						auto reduction = lookaheadIter.front;
1434 
1435 						if (!first && reduction == initialReduction)
1436 							break; // We've looped around using cached results
1437 						first = false;
1438 
1439 						auto digest = hash(reduction);
1440 
1441 						if (digest in cache || digest in lookaheadResults || lookaheadProcesses[].canFind!(p => p.digest == digest))
1442 						{
1443 							bool prediction;
1444 							if (digest in cache)
1445 								prediction = cache[digest];
1446 							else
1447 							if (digest in lookaheadResults)
1448 								prediction = lookaheadResults[digest];
1449 							else
1450 								prediction = lookaheadPredict();
1451 							lookaheadIter.next(prediction);
1452 							if (lookaheadIter.done)
1453 								break;
1454 							continue;
1455 						}
1456 
1457 						process.digest = digest;
1458 
1459 						static int counter;
1460 						process.testdir = dirSuffix("lookahead.%d".format(counter++));
1461 						save(reduction, process.testdir);
1462 
1463 						auto nul = File(nullFileName, "w+");
1464 						process.pid = spawnShell(tester, nul, nul, nul, null, Config.none, process.testdir);
1465 
1466 						lookaheadIter.next(lookaheadPredict());
1467 						break;
1468 					}
1469 				}
1470 			}
1471 
1472 			// Find a result for the current test.
1473 
1474 			auto plookaheadResult = digest in lookaheadResults;
1475 			if (plookaheadResult)
1476 			{
1477 				writeln(*plookaheadResult ? "Yes" : "No", " (lookahead)");
1478 				return *plookaheadResult;
1479 			}
1480 
1481 			foreach (ref process; lookaheadProcesses)
1482 			{
1483 				if (process.pid && process.digest == digest)
1484 				{
1485 					// Current test is already being tested in the background, wait for its result.
1486 
1487 					auto exitCode = process.pid.wait();
1488 
1489 					auto result = reap(process, exitCode);
1490 					writeln(result ? "Yes" : "No", " (lookahead-wait)");
1491 					return result;
1492 				}
1493 			}
1494 		}
1495 
1496 		return fallback;
1497 	}
1498 
1499 	bool doTest()
1500 	{
1501 		string testdir = dirSuffix("test");
1502 		measure!"testSave"({save(reduction, testdir);}); scope(exit) measure!"clean"({safeDelete(testdir);});
1503 
1504 		Pid pid;
1505 		if (noRedirect)
1506 			pid = spawnShell(tester, null, Config.none, testdir);
1507 		else
1508 		{
1509 			auto nul = File(nullFileName, "w+");
1510 			pid = spawnShell(tester, nul, nul, nul, null, Config.none, testdir);
1511 		}
1512 
1513 		bool result;
1514 		measure!"test"({result = pid.wait() == 0;});
1515 		writeln(result ? "Yes" : "No");
1516 		return result;
1517 	}
1518 
1519 	auto result = ramCached(diskCached(lookahead(doTest())));
1520 	if (trace) saveTrace(reduction, dirSuffix("trace"), result);
1521 	return result;
1522 }
1523 
1524 void saveTrace(Reduction reduction, string dir, bool result)
1525 {
1526 	if (!exists(dir)) mkdir(dir);
1527 	static size_t count;
1528 	string countStr = format("%08d-#%08d-%d", count++, reduction.target ? reduction.target.id : 0, result ? 1 : 0);
1529 	auto traceDir = buildPath(dir, countStr);
1530 	save(reduction, traceDir);
1531 }
1532 
1533 void applyNoRemoveMagic()
1534 {
1535 	enum MAGIC_START = "DustMiteNoRemoveStart";
1536 	enum MAGIC_STOP  = "DustMiteNoRemoveStop";
1537 
1538 	bool state = false;
1539 
1540 	bool scanString(string s)
1541 	{
1542 		if (s.length == 0)
1543 			return false;
1544 		if (s.canFind(MAGIC_START))
1545 			state = true;
1546 		if (s.canFind(MAGIC_STOP))
1547 			state = false;
1548 		return state;
1549 	}
1550 
1551 	bool scan(Entity e)
1552 	{
1553 		bool removeThis;
1554 		removeThis  = scanString(e.head);
1555 		foreach (c; e.children)
1556 			removeThis |= scan(c);
1557 		removeThis |= scanString(e.tail);
1558 		e.noRemove |= removeThis;
1559 		return removeThis;
1560 	}
1561 
1562 	scan(root);
1563 }
1564 
1565 void applyNoRemoveRegex(string[] noRemoveStr, string[] reduceOnly)
1566 {
1567 	auto noRemove = array(map!((string s) { return regex(s, "mg"); })(noRemoveStr));
1568 
1569 	void mark(Entity e)
1570 	{
1571 		e.noRemove = true;
1572 		foreach (c; e.children)
1573 			mark(c);
1574 	}
1575 
1576 	auto files = root.isFile ? [root] : root.children;
1577 
1578 	foreach (f; files)
1579 	{
1580 		assert(f.isFile);
1581 
1582 		if
1583 		(
1584 			(reduceOnly.length && !reduceOnly.any!(mask => globMatch(f.filename, mask)))
1585 		||
1586 			(noRemove.any!(a => !match(f.filename, a).empty))
1587 		)
1588 		{
1589 			mark(f);
1590 			root.noRemove = true;
1591 			continue;
1592 		}
1593 
1594 		immutable(char)*[] starts, ends;
1595 
1596 		foreach (r; noRemove)
1597 			foreach (c; match(f.contents, r))
1598 			{
1599 				assert(c.hit.ptr >= f.contents.ptr && c.hit.ptr < f.contents.ptr+f.contents.length);
1600 				starts ~= c.hit.ptr;
1601 				ends ~= c.hit.ptr + c.hit.length;
1602 			}
1603 
1604 		starts.sort();
1605 		ends.sort();
1606 
1607 		int noRemoveLevel = 0;
1608 
1609 		bool scanString(string s)
1610 		{
1611 			if (!s.length)
1612 				return noRemoveLevel > 0;
1613 
1614 			auto start = s.ptr;
1615 			auto end = start + s.length;
1616 			assert(start >= f.contents.ptr && end <= f.contents.ptr+f.contents.length);
1617 
1618 			while (starts.length && starts[0] < end)
1619 			{
1620 				noRemoveLevel++;
1621 				starts = starts[1..$];
1622 			}
1623 			bool result = noRemoveLevel > 0;
1624 			while (ends.length && ends[0] <= end)
1625 			{
1626 				noRemoveLevel--;
1627 				ends = ends[1..$];
1628 			}
1629 			return result;
1630 		}
1631 
1632 		bool scan(Entity e)
1633 		{
1634 			bool result = false;
1635 			if (scanString(e.head))
1636 				result = true;
1637 			foreach (c; e.children)
1638 				if (scan(c))
1639 					result = true;
1640 			if (scanString(e.tail))
1641 				result = true;
1642 			if (result)
1643 				e.noRemove = root.noRemove = true;
1644 			return result;
1645 		}
1646 
1647 		scan(f);
1648 	}
1649 }
1650 
1651 void applyNoRemoveDeps()
1652 {
1653 	static void applyDeps(Entity e)
1654 	{
1655 		e.noRemove = true;
1656 		foreach (d; e.dependencies)
1657 			applyDeps(d);
1658 	}
1659 
1660 	static void scan(Entity e)
1661 	{
1662 		if (e.noRemove)
1663 			applyDeps(e);
1664 		foreach (c; e.children)
1665 			scan(c);
1666 	}
1667 
1668 	scan(root);
1669 
1670 	// Propagate upwards
1671 	static bool fill(Entity e)
1672 	{
1673 		foreach (c; e.children)
1674 			e.noRemove |= fill(c);
1675 		return e.noRemove;
1676 	}
1677 
1678 	fill(root);
1679 }
1680 
1681 void loadCoverage(string dir)
1682 {
1683 	void scanFile(Entity f)
1684 	{
1685 		auto fn = buildPath(dir, setExtension(baseName(f.filename), "lst"));
1686 		if (!exists(fn))
1687 			return;
1688 		writeln("Loading coverage file ", fn);
1689 
1690 		static bool covered(string line)
1691 		{
1692 			enforce(line.length >= 8 && line[7]=='|', "Invalid syntax in coverage file");
1693 			line = line[0..7];
1694 			return line != "0000000" && line != "       ";
1695 		}
1696 
1697 		auto lines = map!covered(splitLines(readText(fn))[0..$-1]);
1698 		uint line = 0;
1699 
1700 		bool coverString(string s)
1701 		{
1702 			bool result;
1703 			foreach (char c; s)
1704 			{
1705 				result |= lines[line];
1706 				if (c == '\n')
1707 					line++;
1708 			}
1709 			return result;
1710 		}
1711 
1712 		bool cover(ref Entity e)
1713 		{
1714 			bool result;
1715 			result |= coverString(e.head);
1716 			foreach (ref c; e.children)
1717 				result |= cover(c);
1718 			result |= coverString(e.tail);
1719 
1720 			e.noRemove |= result;
1721 			return result;
1722 		}
1723 
1724 		foreach (ref c; f.children)
1725 			f.noRemove |= cover(c);
1726 	}
1727 
1728 	void scanFiles(Entity e)
1729 	{
1730 		if (e.isFile)
1731 			scanFile(e);
1732 		else
1733 			foreach (c; e.children)
1734 				scanFiles(c);
1735 	}
1736 
1737 	scanFiles(root);
1738 }
1739 
1740 void assignID(Entity e)
1741 {
1742 	static int counter;
1743 	e.id = ++counter;
1744 	foreach (c; e.children)
1745 		assignID(c);
1746 }
1747 
1748 void dumpSet(string fn)
1749 {
1750 	auto f = File(fn, "wb");
1751 
1752 	string printable(string s) { return s is null ? "null" : `"` ~ s.replace("\\", `\\`).replace("\"", `\"`).replace("\r", `\r`).replace("\n", `\n`) ~ `"`; }
1753 	string printableFN(string s) { return "/*** " ~ s ~ " ***/"; }
1754 
1755 	bool[int] dependents;
1756 	void scanDependents(Entity e)
1757 	{
1758 		foreach (d; e.dependencies)
1759 			dependents[d.id] = true;
1760 		foreach (c; e.children)
1761 			scanDependents(c);
1762 	}
1763 	scanDependents(root);
1764 
1765 	void print(Entity e, int depth)
1766 	{
1767 		auto prefix = replicate("  ", depth);
1768 
1769 		// if (!fileLevel) { f.writeln(prefix, "[ ... ]"); continue; }
1770 
1771 		f.write(prefix);
1772 		if (e.children.length == 0)
1773 		{
1774 			f.write(
1775 				"[",
1776 				e.noRemove ? "!" : "",
1777 				" ",
1778 				e.isFile ? e.filename ? printableFN(e.filename) ~ " " : null : e.head ? printable(e.head) ~ " " : null,
1779 				e.tail ? printable(e.tail) ~ " " : null,
1780 				e.comment ? "/* " ~ e.comment ~ " */ " : null,
1781 				"]"
1782 			);
1783 		}
1784 		else
1785 		{
1786 			f.writeln("[", e.noRemove ? "!" : "", e.comment ? " // " ~ e.comment : null);
1787 			if (e.isFile) f.writeln(prefix, "  ", printableFN(e.filename));
1788 			if (e.head) f.writeln(prefix, "  ", printable(e.head));
1789 			foreach (c; e.children)
1790 				print(c, depth+1);
1791 			if (e.tail) f.writeln(prefix, "  ", printable(e.tail));
1792 			f.write(prefix, "]");
1793 		}
1794 		if (e.id in dependents || trace)
1795 			f.write(" =", e.id);
1796 		if (e.dependencies.length)
1797 		{
1798 			f.write(" =>");
1799 			foreach (d; e.dependencies)
1800 				f.write(" ", d.id);
1801 		}
1802 		f.writeln();
1803 	}
1804 
1805 	print(root, 0);
1806 
1807 	f.close();
1808 }
1809 
1810 void dumpToHtml(string fn)
1811 {
1812 	auto buf = appender!string();
1813 
1814 	void dumpText(string s)
1815 	{
1816 		foreach (c; s)
1817 			switch (c)
1818 			{
1819 				case '<':
1820 					buf.put("&lt;");
1821 					break;
1822 				case '>':
1823 					buf.put("&gt;");
1824 					break;
1825 				case '&':
1826 					buf.put("&amp;");
1827 					break;
1828 				default:
1829 					buf.put(c);
1830 			}
1831 	}
1832 
1833 	void dump(Entity e)
1834 	{
1835 		if (e.isFile)
1836 		{
1837 			buf.put("<h1>");
1838 			dumpText(e.filename);
1839 			buf.put("</h1><pre>");
1840 			foreach (c; e.children)
1841 				dump(c);
1842 			buf.put("</pre>");
1843 		}
1844 		else
1845 		{
1846 			buf.put("<span>");
1847 			dumpText(e.head);
1848 			foreach (c; e.children)
1849 				dump(c);
1850 			dumpText(e.tail);
1851 			buf.put("</span>");
1852 		}
1853 	}
1854 
1855 	buf.put(q"EOT
1856 <style> pre span:hover { outline: 1px solid rgba(0,0,0,0.2); background-color: rgba(100,100,100,0.1	); } </style>
1857 EOT");
1858 
1859 	dump(root);
1860 
1861 	std.file.write(fn, buf.data());
1862 }
1863 
1864 void dumpText(string fn, ref Reduction r = nullReduction)
1865 {
1866 	auto f = File(fn, "wt");
1867 	dump(root, r, (string) {}, &f.write!string);
1868 	f.close();
1869 }