Chromium Code Reviews| Index: sdk/lib/_internal/pub/lib/src/solver/backtracking_solver.dart |
| diff --git a/sdk/lib/_internal/pub/lib/src/solver/backtracking_solver.dart b/sdk/lib/_internal/pub/lib/src/solver/backtracking_solver.dart |
| index 43009095b41b46043eac4fc0149de5d19de2ad4f..8439c9eb42aa69aeb019afe4eb31ea50a856426d 100644 |
| --- a/sdk/lib/_internal/pub/lib/src/solver/backtracking_solver.dart |
| +++ b/sdk/lib/_internal/pub/lib/src/solver/backtracking_solver.dart |
| @@ -44,6 +44,8 @@ import '../sdk.dart' as sdk; |
| import '../source_registry.dart'; |
| import '../utils.dart'; |
| import '../version.dart'; |
| +import 'dependency_queue.dart'; |
| +import 'version_queue.dart'; |
| import 'version_solver.dart'; |
| /// The top-level solver. Keeps track of the current potential solution, and |
| @@ -71,8 +73,8 @@ class BacktrackingSolver { |
| /// are valid. This keeps track of which versions have been selected so far |
| /// and which remain to be tried. |
| /// |
| - /// Each entry in the list is an ordered [Queue] of versions to try for a |
| - /// single package. The first item in the queue is the currently selected |
| + /// Each entry in the list is [VersionQueue], which is an ordered queue of |
|
nweiz
2013/11/26 21:00:43
"is [VersionQueue]" -> "is a [VersionQueue]"
Bob Nystrom
2013/11/26 22:16:10
Done.
|
| + /// versions to try for a single package. It maintains the currently selected |
| /// version for that package. When a new dependency is encountered, a queue |
| /// of versions of that dependency is pushed onto the end of the list. A |
| /// queue is removed from the list once it's empty, indicating that none of |
| @@ -83,7 +85,7 @@ class BacktrackingSolver { |
| /// on an already-selected package, and that constraint doesn't match the |
| /// selected version, that will cause the current solution to fail and |
| /// trigger backtracking. |
| - final _selected = <Queue<PackageId>>[]; |
| + final _selected = <VersionQueue>[]; |
| /// The number of solutions the solver has tried so far. |
| int get attemptedSolutions => _attemptedSolutions; |
| @@ -154,10 +156,10 @@ class BacktrackingSolver { |
| /// in the list will be the currently selected version of that package. |
| /// Subsequent items will be tried if it the current selection fails. Returns |
| /// the first selected version. |
| - PackageId select(Iterable<PackageId> versions) { |
| - _selected.add(new Queue<PackageId>.from(versions)); |
| + PackageId select(VersionQueue versions) { |
| + _selected.add(versions); |
| logSolve(); |
| - return versions.first; |
| + return versions.current; |
| } |
| /// Returns the the currently selected id for the package [name] or `null` if |
| @@ -168,7 +170,7 @@ class BacktrackingSolver { |
| // Look through the current selections. |
| for (var i = _selected.length - 1; i >= 0; i--) { |
| - if (_selected[i].first.name == name) return _selected[i].first; |
| + if (_selected[i].current.name == name) return _selected[i].current; |
| } |
| return null; |
| @@ -188,13 +190,15 @@ class BacktrackingSolver { |
| return new Traverser(this).traverse().catchError((error) { |
| if (error is! SolveFailure) throw error; |
| - if (_backtrack(error)) { |
| - _attemptedSolutions++; |
| - return _traverseSolution(); |
| - } |
| + return _backtrack(error).then((canTry) { |
| + if (canTry) { |
| + _attemptedSolutions++; |
| + return _traverseSolution(); |
| + } |
| - // All out of solutions, so fail. |
| - throw error; |
| + // All out of solutions, so fail. |
| + throw error; |
| + }); |
| }); |
| }); |
| @@ -204,40 +208,47 @@ class BacktrackingSolver { |
| /// the next possible solution. |
| /// |
| /// Returns `true` if there is a new solution to try. |
| - bool _backtrack(SolveFailure failure) { |
| - var dependers = failure.dependencies.map((dep) => dep.depender).toSet(); |
| - |
| - while (!_selected.isEmpty) { |
| - _backjump(failure); |
| + Future<bool> _backtrack(SolveFailure failure) { |
| + // Bail if there is nothing to backtrack to. |
| + if (_selected.isEmpty) return new Future.value(false); |
| + |
| + // Get the set of packages that may have led to this failure. |
| + var dependers = _getTransitiveDependers(failure.package); |
| + |
| + // Advance past the current version of the leaf-most package. |
| + advanceVersion() { |
| + _backjump(failure, dependers); |
| + var previous = _selected.last.current; |
| + return _selected.last.advance().then((success) { |
| + if (success) { |
| + logSolve(); |
| + return true; |
| + } |
| - // Advance past the current version of the leaf-most package. |
| - var previous = _selected.last.removeFirst(); |
| - if (!_selected.last.isEmpty) { |
| - logSolve(); |
| - return true; |
| - } |
| + logSolve('$previous is last version, backtracking'); |
| - logSolve('$previous is last version, backtracking'); |
| + // That package has no more versions, so pop it and try the next one. |
| + _selected.removeLast(); |
| + if (_selected.isEmpty) return false; |
| - // That package has no more versions, so pop it and try the next one. |
| - _selected.removeLast(); |
| + // If we got here, the leafmost package was discarded so we need to |
| + // advance the next one. |
| + return advanceVersion(); |
| + }); |
| } |
| - return false; |
| + return advanceVersion(); |
| } |
| /// Walks the selected packages from most to least recent to determine which |
| /// ones can be ignored and jumped over by the backtracker. The only packages |
| - /// we need to backtrack to are ones that have other versions to try and that |
| - /// led (possibly indirectly) to the failure. Everything else can be skipped. |
| - void _backjump(SolveFailure failure) { |
| + /// we need to backtrack to are ones that led (possibly indirectly) to the |
| + /// failure. Everything else can be skipped. |
| + void _backjump(SolveFailure failure, Set<String> dependers) { |
| for (var i = _selected.length - 1; i >= 0; i--) { |
| // Each queue will never be empty since it gets discarded by _backtrack() |
| // when that happens. |
| - var selected = _selected[i].first; |
| - |
| - // If the package has no more versions, we can jump over it. |
| - if (_selected[i].length == 1) continue; |
| + var selected = _selected[i].current; |
| // If we get to the package that failed, backtrack to here. |
| if (selected.name == failure.package) { |
| @@ -248,10 +259,9 @@ class BacktrackingSolver { |
| // If we get to a package that depends on the failing package, backtrack |
| // to here. |
| - var path = _getDependencyPath(selected, failure.package); |
| - if (path != null) { |
| + if (dependers.contains(selected.name)) { |
| logSolve('backjump to ${selected.name} because it depends on ' |
| - '${failure.package} along $path'); |
| + '${failure.package}'); |
| _selected.removeRange(i + 1, _selected.length); |
| return; |
| } |
| @@ -264,40 +274,54 @@ class BacktrackingSolver { |
| _selected.removeRange(1, _selected.length); |
| } |
| - /// Determines if [depender] has a direct or indirect dependency on |
| - /// [dependent] based on the currently selected versions of all packages. |
| - /// Returns a string describing the dependency chain if it does, or `null` if |
| - /// there is no dependency. |
| - String _getDependencyPath(PackageId depender, String dependent) { |
| - // TODO(rnystrom): This is O(n^2) where n is the number of selected |
| - // packages. Could store the reverse dependency graph to address that. If |
| - // we do that, we need to make sure it gets correctly rolled back when |
| - // backtracking occurs. |
| - var visited = new Set<String>(); |
| - |
| - walkDeps(PackageId package, String currentPath) { |
| - if (visited.contains(package.name)) return null; |
| - visited.add(package.name); |
| - |
| - var pubspec = cache.getCachedPubspec(package); |
| - if (pubspec == null) return null; |
| + /// Gets the set of currently selected packages that depend on [dependency] |
| + /// either directly or indirectly. |
| + /// |
| + /// When backtracking, it's only useful to consider changing the version of |
| + /// packages who have a dependency on the failed package that triggered |
| + /// backtracking. This is used to determine those packages. |
| + /// |
| + /// We calculate the full set up front before backtracking because during |
| + /// backtracking, we will unselect packages and start to lose this |
| + /// information in the middle of the process. |
| + /// |
| + /// For example, consider dependencies A -> B -> C. We've selected A and B |
| + /// then encounter a problem with C. We start backtracking. B has no more |
| + /// versions so we discard it and keep backtracking to A. When we get there, |
| + /// since we've unselected B, we no longer realize that A had a transitive |
| + /// dependency on C. We would end up backjumping over A and failing. |
| + /// |
| + /// Calculating the dependency set up front before we start backtracking |
| + /// solves that. |
| + Set<String> _getTransitiveDependers(String dependency) { |
| + // Generate a reverse dependency graph. For each package, create edges to |
| + // each package that depends on it. |
| + var dependers = new Map<String, Set<String>>(); |
| + for (var i = 0; i < _selected.length; i++) { |
| + var id = _selected[i].current; |
| + var pubspec = cache.getCachedPubspec(id); |
| + if (pubspec == null) continue; |
| + dependers.putIfAbsent(id.name, () => new Set<String>()); |
| for (var dep in pubspec.dependencies) { |
| - if (dep.name == dependent) return currentPath; |
| - |
| - var selected = getSelected(dep.name); |
| - // Ignore unselected dependencies. We haven't traversed into them yet, |
| - // so they can't affect backjumping. |
| - if (selected == null) continue; |
| - |
| - var depPath = walkDeps(selected, '$currentPath -> ${dep.name}'); |
| - if (depPath != null) return depPath; |
| + var depender = dependers.putIfAbsent(dep.name, |
| + () => new Set<String>()); |
| + depender.add(id.name); |
| } |
| + } |
| - return null; |
| + // Now walk the depending graph to see which packages transitively depend |
| + // on [dependency]. |
| + var visited = new Set<String>(); |
| + walk(String package) { |
| + // Don't get stuck in cycles. |
| + if (visited.contains(package)) return; |
| + visited.add(package); |
| + var depender = dependers[package].forEach(walk); |
| } |
| - return walkDeps(depender, depender.name); |
| + walk(dependency); |
| + return visited; |
| } |
| /// Logs the initial parameters to the solver. |
| @@ -324,8 +348,7 @@ class BacktrackingSolver { |
| if (_selected.isEmpty) { |
| message = "* start at root"; |
| } else { |
| - var count = _selected.last.length; |
| - message = "* select ${_selected.last.first} ($count versions)"; |
| + message = "* select ${_selected.last.current}"; |
| } |
| } else { |
| // Otherwise, indent it under the current selected package. |
| @@ -424,79 +447,79 @@ class Traverser { |
| } |
| } |
| - // Given a package dep, returns a future that completes to a pair of the |
| - // dep and the number of versions available for it. |
| - getNumVersions(PackageDep dep) { |
| - // There is only ever one version of the root package. |
| - if (dep.isRoot) { |
| - return new Future.value(new Pair<PackageDep, int>(dep, 1)); |
| - } |
| - |
| - return _solver.cache.getVersions(dep.toRef()).then((versions) { |
| - return new Pair<PackageDep, int>(dep, versions.length); |
| - }).catchError((error, trace) { |
| - // If it fails for any reason, just treat that as no versions. This |
| - // will sort this reference higher so that we can traverse into it |
| - // and report the error more properly. |
| - log.solver("Could not get versions for $dep:\n$error\n\n$trace"); |
| - return new Pair<PackageDep, int>(dep, 0); |
| - }); |
| - } |
| - |
| - return Future.wait(deps.map(getNumVersions)).then((pairs) { |
| - // Future.wait() returns an immutable list, so make a copy. |
| - pairs = pairs.toList(); |
| - |
| - // Sort in best-first order to minimize backtracking. |
| - pairs.sort((a, b) { |
| - // Traverse into packages we've already selected first. |
| - var aIsSelected = _solver.getSelected(a.first.name) != null; |
| - var bIsSelected = _solver.getSelected(b.first.name) != null; |
| - if (aIsSelected && !bIsSelected) return -1; |
| - if (!aIsSelected && bIsSelected) return 1; |
| - |
| - // Traverse into packages with fewer versions since they will lead to |
| - // less backtracking. |
| - if (a.last != b.last) return a.last.compareTo(b.last); |
| - |
| - // Otherwise, just sort by name so that it's deterministic. |
| - return a.first.name.compareTo(b.first.name); |
| - }); |
| - |
| - var queue = new Queue<PackageDep>.from(pairs.map((pair) => pair.first)); |
| - return _traverseDeps(id.name, queue); |
| - }); |
| + return _traverseDeps(id.name, new DependencyQueue(_solver, deps)); |
| }); |
| } |
| /// Traverses the references that [depender] depends on, stored in [deps]. |
| + /// |
| /// Desctructively modifies [deps]. Completes to a list of packages if the |
| /// traversal is complete. Completes it to an error if a failure occurred. |
| /// Otherwise, recurses. |
| - Future<List<PackageId>> _traverseDeps(String depender, |
| - Queue<PackageDep> deps) { |
| + Future<List<PackageId>> _traverseDeps(String depender, DependencyQueue deps) { |
| // Move onto the next package if we've traversed all of these references. |
| if (deps.isEmpty) return _traversePackage(); |
| return resetStack(() { |
| - var dep = deps.removeFirst(); |
| + return deps.advance().then((dep) { |
| + _validateDependency(dep, depender); |
| + |
| + // Add the dependency. |
| + var dependencies = _getDependencies(dep.name); |
| + dependencies.add(new Dependency(depender, dep)); |
| - _validateDependency(dep, depender); |
| - var constraint = _addConstraint(dep, depender); |
| + var constraint = _getConstraint(dep.name); |
| - var selected = _validateSelected(dep, constraint); |
| - if (selected != null) { |
| - // The selected package version is good, so enqueue it to traverse into |
| - // it. |
| - _packages.add(selected); |
| - return _traverseDeps(depender, deps); |
| + // See if it's possible for a package to match that constraint. |
| + if (constraint.isEmpty) { |
| + _solver.logSolve('disjoint constraints on ${dep.name}}'); |
| + throw new DisjointConstraintException(depender, dependencies); |
| + } |
| + |
| + var selected = _validateSelected(dep, constraint); |
| + if (selected != null) { |
| + // The selected package version is good, so enqueue it to traverse |
| + // into it. |
| + _packages.add(selected); |
| + return _traverseDeps(depender, deps); |
| + } |
| + |
| + // We haven't selected a version. Try all of the versions that match |
| + // the constraints we currently have for this package. |
| + var locked = _getValidLocked(dep.name); |
| + |
| + return VersionQueue.create(locked, |
| + () => _getAllowedVersions(dep)).then((versions) { |
| + _packages.add(_solver.select(versions)); |
| + }); |
| + }).then((_) => _traverseDeps(depender, deps)); |
| + }); |
| + } |
| + |
| + /// Gets all versions of [dep] that match the current constraints placed on |
| + /// it. |
| + Future<Iterable<PackageId>> _getAllowedVersions(PackageDep dep) { |
| + var constraint = _getConstraint(dep.name); |
| + return _solver.cache.getVersions(dep.toRef()).then((versions) { |
| + var allowed = versions.where((id) => constraint.allows(id.version)); |
| + |
| + if (allowed.isEmpty) { |
| + _solver.logSolve('no versions for ${dep.name} match $constraint'); |
| + throw new NoVersionException(dep.name, constraint, |
| + _getDependencies(dep.name)); |
| + } |
| + |
| + // If we're doing an upgrade on this package, only allow the latest |
| + // version. |
| + if (_solver._forceLatest.contains(dep.name)) allowed = [allowed.first]; |
| + |
| + // Remove the locked version, if any, since that was already handled. |
| + var locked = _getValidLocked(dep.name); |
| + if (locked != null) { |
| + allowed = allowed.where((dep) => dep.version != locked.version); |
| } |
| - // We haven't selected a version. Get all of the versions that match the |
| - // constraints we currently have for this package and add them to the |
| - // set of solutions to try. |
| - return _selectPackage(dep, constraint).then( |
| - (_) => _traverseDeps(depender, deps)); |
| + return allowed; |
| }); |
| } |
| @@ -527,31 +550,6 @@ class Traverser { |
| } |
| } |
| - /// Adds the version constraint that [depender] places on [dep] to the |
| - /// overall constraint that all shared dependencies place on [dep]. Throws a |
| - /// [SolveFailure] if that results in an unsolvable constraints. |
| - /// |
| - /// Returns the combined [VersionConstraint] that all dependers place on the |
| - /// package. |
| - VersionConstraint _addConstraint(PackageDep dep, String depender) { |
| - // Add the dependency. |
| - var dependencies = _getDependencies(dep.name); |
| - dependencies.add(new Dependency(depender, dep)); |
| - |
| - // Determine the overall version constraint. |
| - var constraint = dependencies |
| - .map((dep) => dep.dep.constraint) |
| - .fold(VersionConstraint.any, (a, b) => a.intersect(b)); |
| - |
| - // See if it's possible for a package to match that constraint. |
| - if (constraint.isEmpty) { |
| - _solver.logSolve('disjoint constraints on ${dep.name}'); |
| - throw new DisjointConstraintException(depender, dependencies); |
| - } |
| - |
| - return constraint; |
| - } |
| - |
| /// Validates the currently selected package against the new dependency that |
| /// [dep] and [constraint] place on it. Returns `null` if there is no |
| /// currently selected package, throws a [SolveFailure] if the new reference |
| @@ -571,42 +569,6 @@ class Traverser { |
| return selected; |
| } |
| - /// Tries to select a package that matches [dep] and [constraint]. Updates |
| - /// the solver state so that we can backtrack from this decision if it turns |
| - /// out wrong, but continues traversing with the new selection. |
| - /// |
| - /// Returns a future that completes with a [SolveFailure] if a version |
| - /// could not be selected or that completes successfully if a package was |
| - /// selected and traversing should continue. |
| - Future _selectPackage(PackageDep dep, VersionConstraint constraint) { |
| - return _solver.cache.getVersions(dep.toRef()).then((versions) { |
| - var allowed = versions.where((id) => constraint.allows(id.version)); |
| - |
| - // See if it's in the lockfile. If so, try that version first. If the |
| - // locked version doesn't match our constraint, just ignore it. |
| - var locked = _getValidLocked(dep.name, constraint); |
| - if (locked != null) { |
| - allowed = allowed.where((dep) => dep.version != locked.version) |
| - .toList(); |
| - allowed.insert(0, locked); |
| - } |
| - |
| - if (allowed.isEmpty) { |
| - _solver.logSolve('no versions for ${dep.name} match $constraint'); |
| - throw new NoVersionException(dep.name, constraint, |
| - _getDependencies(dep.name)); |
| - } |
| - |
| - // If we're doing an upgrade on this package, only allow the latest |
| - // version. |
| - if (_solver._forceLatest.contains(dep.name)) allowed = [allowed.first]; |
| - |
| - // Try the first package in the allowed set and keep track of the list of |
| - // other possible versions in case that fails. |
| - _packages.add(_solver.select(allowed)); |
| - }); |
| - } |
| - |
| /// Gets the list of dependencies for package [name]. Will create an empty |
| /// list if needed. |
| List<Dependency> _getDependencies(String name) { |
| @@ -629,13 +591,22 @@ class Traverser { |
| .firstWhere((dep) => !dep.dep.isRoot, orElse: () => null); |
| } |
| + /// Gets the combined [VersionConstraint] currently being placed on package |
| + /// [name]. |
| + VersionConstraint _getConstraint(String name) { |
| + return _getDependencies(name) |
| + .map((dep) => dep.dep.constraint) |
| + .fold(VersionConstraint.any, (a, b) => a.intersect(b)); |
| + } |
| + |
| /// Gets the package [name] that's currently contained in the lockfile if it |
| /// meets [constraint] and has the same source and description as other |
| /// references to that package. Returns `null` otherwise. |
| - PackageId _getValidLocked(String name, VersionConstraint constraint) { |
| + PackageId _getValidLocked(String name) { |
| var package = _solver.getLocked(name); |
| if (package == null) return null; |
| + var constraint = _getConstraint(name); |
| if (!constraint.allows(package.version)) { |
| _solver.logSolve('$package is locked but does not match $constraint'); |
| return null; |