package driver; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import files.RootedSourcePath; /** * This class resolves the dependencies between source files used at * compile-time. * * Macro definitions are contained in macro files (.javam extension). There may * be zero or more macro definitions in one such file. * * There is currently no visibility control mechanism for macros, which means * that all macros are accessible from everywhere. Macros do however behave like * classes when it comes to accessing other classes (this comes from the fact * that macros are compiled into classes). * * Macro files may depend on other macro files or on regular source files (to be * used at compile-time). The regular source files may themselves depend on * macros or on other source files. We need to find an ordering of source files * such that all dependencies are met and no loops occur. * * The mechanism through which dependencies between macros and sources files are * defined is the require statement, a modified form of import statement. You * can learn more about the require statement in the javadoc of * {@link RequiresParser}. * * Macro files and source files used at compile time can also rely on * pre-compiled classes without hurdles (using the regular import statements). * The compiled classes (.class or .jar) just needs to be retrievable via * caxap's own classpath. * * It is important to note that due to the way dependencies are resolved, the * prelude (package declaration + import/require statements) in each file used * at compile-time can't contain macro calls. * * If a file requires a macro, the macro can be used anywhere in the file * (except, as said above, in the prelude of files used at compile-time). A * macro may be used in the file that defines it after the definition is * complete. A given macro's call cannot appear in its own expansion code, * neither can another macro's call which expand to a call of the given macro. * Macros whose expansion code use the given macro can't be called either. */ public class DependencyResolver { /***************************************************************************** * Finds an ordering of macro and regular source files that satisfies all * compile-time dependencies. If no such ordering can be found - because * there is a circular dependency - throws an error. * * {@see {@link DependencyResolver}} */ public static List<SourceFile> resolve( List<RootedSourcePath> macroPaths, List<RootedSourcePath> sourcePaths) { return new DependencyResolver().run(macroPaths, sourcePaths); } /****************************************************************************/ private final SourceRepository repo = Context.get().repo; /****************************************************************************/ private List<SourceFile> run( List<RootedSourcePath> macroPaths, List<RootedSourcePath> sourcePaths) { repo.hint(macroPaths); repo.hint(sourcePaths); List<SourceFile> files = new ArrayList<>(); for (RootedSourcePath path : macroPaths) { files.add(repo.get(path)); } Set <SourceFile> set = detectCycles(files); List<SourceFile> ordered = parseOrdering(set); for (SourceFile file : ordered) { file.declareUsedAtCompileTime(); } for (RootedSourcePath path : sourcePaths) { SourceFile file = repo.get(path); if (!set.contains(file)) { ordered.add(file); } } return ordered; } /***************************************************************************** * Detect cycles in files dependencies, using the direct dependencies stored * in the Requires object associated to each SourceFile. If a cycle is * encountered, throws an error. Returns the set of visited SourceFile. * * Checks for cycles by walking the dependency graph recursively, starting * from each file (= each node in the graph). This would normally result in * passing many times over the same file. But once a file has been checked, it * is guaranteed not to belong to any cycle. Therefore, we keep a set of * checked file to avoid to duplicate work. */ private Set<SourceFile> detectCycles(List<SourceFile> files) { Set<SourceFile> checked = new HashSet<>(); for (SourceFile file : files) if (!checked.contains(file)) { detectCycles(file, checked, new HashSet<SourceFile>()); checked.add(file); } return checked; } /***************************************************************************** * Detects cycles by recursively walking $file and keeping files on the * current path in $visited. $checked is explained in * {@link DependencyResolver#detectCycles()}. */ private void detectCycles( SourceFile file, Set<SourceFile> checked, Set<SourceFile> visited) { for (SourceFile dependency : file.requires().dependencies()) if (!checked.contains(dependency)) { // if visited did already contain $dependency if (!visited.add(dependency)) { throw new Error("Dependency loop detect for macro class \"" + dependency + "\", via macro class \"" + file + "\"."); } detectCycles(dependency, checked, visited); visited.remove(dependency); checked.add(dependency); } } /***************************************************************************** * Return an ordering of $files that respects the dependencies between them. * * If the need should arise, the time complexity of this algorithm can be * diminished by adding a HashSet that mirrors the content of the list. */ private List<SourceFile> parseOrdering(Collection<SourceFile> files) { List<SourceFile> ordering = new ArrayList<>(files.size()); for (SourceFile file : files) if (!ordering.contains(file)) { parseOrdering(file, ordering); } return ordering; } /***************************************************************************** * Adds $file to $ordering, ensuring that the dependencies of $file precedes * it in the $ordering, adding them recursively if needed. */ private void parseOrdering(SourceFile file, List<SourceFile> ordering) { for (SourceFile dependency : file.requires().dependencies()) if (!ordering.contains(dependency)) { parseOrdering(dependency, ordering); } ordering.add(file); } }