diff --git a/src/internal/core.rs b/src/internal/core.rs index 26a699d9..9e6d2181 100644 --- a/src/internal/core.rs +++ b/src/internal/core.rs @@ -82,7 +82,7 @@ impl State { ) { let dep_incompats = self.add_incompatibility_from_dependencies(package, version.clone(), dependencies); - self.partial_solution.add_package_version_incompatibilities( + self.partial_solution.add_package_version_dependencies( package, version.clone(), dep_incompats, @@ -249,7 +249,8 @@ impl State { } } - /// Backtracking. + /// After a conflict occurred, backtrack the partial solution to a given decision level, and add + /// the incompatibility if it was new. fn backtrack( &mut self, incompat: IncompDpId, @@ -265,6 +266,21 @@ impl State { } } + /// Manually backtrack before the given package was selected. + /// + /// This can be used to switch the order of packages if the previous prioritization was bad. + /// + /// Returns the number of the decisions that were backtracked, or `None` if the package was not + /// decided on yet. + pub fn backtrack_package(&mut self, package: Id) -> Option { + let base_decision_level = self.partial_solution.current_decision_level(); + let new_decision_level = self.partial_solution.backtrack_package(package).ok()?; + // Remove contradicted incompatibilities that depend on decisions we just backtracked away. + self.contradicted_incompatibilities + .retain(|_, dl| *dl <= new_decision_level); + Some(base_decision_level.0 - new_decision_level.0) + } + /// Add this incompatibility into the set of all incompatibilities. /// /// PubGrub collapses identical dependencies from adjacent package versions diff --git a/src/internal/partial_solution.rs b/src/internal/partial_solution.rs index 9804dcb8..d03506d6 100644 --- a/src/internal/partial_solution.rs +++ b/src/internal/partial_solution.rs @@ -6,6 +6,7 @@ use std::fmt::{Debug, Display}; use std::hash::BuildHasherDefault; +use log::debug; use priority_queue::PriorityQueue; use rustc_hash::FxHasher; @@ -377,12 +378,37 @@ impl PartialSolution { self.has_ever_backtracked = true; } - /// We can add the version to the partial solution as a decision - /// if it doesn't produce any conflict with the new incompatibilities. - /// In practice I think it can only produce a conflict if one of the dependencies - /// (which are used to make the new incompatibilities) - /// is already in the partial solution with an incompatible version. - pub(crate) fn add_package_version_incompatibilities( + /// Backtrack the partial solution before a particular package was selected. + /// + /// This can be used to switch the order of packages if the previous prioritization was bad. + /// + /// Returns the new decision level on success and an error if the package was not decided on + /// yet. + pub(crate) fn backtrack_package(&mut self, package: Id) -> Result { + let Some(decision_level) = self.package_assignments.get_index_of(&package) else { + return Err(()); + }; + let decision_level = DecisionLevel(decision_level as u32); + if decision_level > self.current_decision_level { + return Err(()); + } + debug!( + "Package backtracking ot decision level {}", + decision_level.0 + ); + self.backtrack(decision_level); + Ok(decision_level) + } + + /// Add a package version as decision if none of its dependencies conflicts with the partial + /// solution. + /// + /// If the resolution never backtracked before, a fast path adds the package version directly + /// without checking dependencies. + /// + /// Returns the incompatibility that caused the current version to be rejected, if a conflict + /// in the dependencies was found. + pub(crate) fn add_package_version_dependencies( &mut self, package: Id, version: DP::V,