Skip to content

Commit 7a59690

Browse files
authored
use intermediate satisfier causes in priority statistics (#291)
* use intermediate satisfier causes in priority statistics * better wording for the comment
1 parent a8e2ba6 commit 7a59690

File tree

4 files changed

+57
-24
lines changed

4 files changed

+57
-24
lines changed

src/internal/core.rs

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ impl<DP: DependencyProvider> State<DP> {
133133
&mut self,
134134
package: Id<DP::P>,
135135
) -> Result<SmallVec<(Id<DP::P>, IncompDpId<DP>)>, NoSolutionError<DP>> {
136-
let mut root_causes = SmallVec::default();
136+
let mut satisfier_causes = SmallVec::default();
137137
self.unit_propagation_buffer.clear();
138138
self.unit_propagation_buffer.push(package);
139139
while let Some(current_package) = self.unit_propagation_buffer.pop() {
@@ -186,12 +186,11 @@ impl<DP: DependencyProvider> State<DP> {
186186
}
187187
}
188188
if let Some(incompat_id) = conflict_id {
189-
let (package_almost, root_cause) =
190-
self.conflict_resolution(incompat_id)
191-
.map_err(|terminal_incompat_id| {
192-
self.build_derivation_tree(terminal_incompat_id)
193-
})?;
194-
root_causes.push((package, root_cause));
189+
let (package_almost, root_cause) = self
190+
.conflict_resolution(incompat_id, &mut satisfier_causes)
191+
.map_err(|terminal_incompat_id| {
192+
self.build_derivation_tree(terminal_incompat_id)
193+
})?;
195194
self.unit_propagation_buffer.clear();
196195
self.unit_propagation_buffer.push(package_almost);
197196
// Add to the partial solution with incompat as cause.
@@ -207,16 +206,45 @@ impl<DP: DependencyProvider> State<DP> {
207206
}
208207
}
209208
// If there are no more changed packages, unit propagation is done.
210-
Ok(root_causes)
209+
Ok(satisfier_causes)
211210
}
212211

213-
/// Return the root cause or the terminal incompatibility.
214-
/// CF <https://github.com/dart-lang/pub/blob/master/doc/solver.md#unit-propagation>
212+
/// Return the root cause or the terminal incompatibility. CF
213+
/// <https://github.com/dart-lang/pub/blob/master/doc/solver.md#unit-propagation>
214+
///
215+
/// When we found a conflict, we want to learn as much as possible from it, to avoid making (or
216+
/// keeping) decisions that will be rejected. Say we found that the dependency requirements on X and the
217+
/// dependency requirements on Y are incompatible. We may find that the decisions on earlier packages B and C
218+
/// require us to make incompatible requirements on X and Y, so we backtrack until either B or C
219+
/// can be revisited. To make it practical, we really only need one of the terms to be a
220+
/// decision. We may as well leave the other terms general. Something like "the dependency on
221+
/// the package X is incompatible with the decision on C" tends to work out pretty well. Then if
222+
/// A turns out to also have a dependency on X the resulting root cause is still useful.
223+
/// (`unit_propagation` will ensure we don't try that version of C.)
224+
/// Of course, this is more heuristics than science. If the output is too general, then
225+
/// `unit_propagation` will handle the confusion by calling us again with the next most specific
226+
/// conflict it comes across. If the output is too specific, then the outer `solver` loop will
227+
/// eventually end up calling us again until all possibilities are enumerated.
228+
///
229+
/// To end up with a more useful incompatibility, this function combines incompatibilities into
230+
/// derivations. Fulfilling this derivation implies the later conflict. By banning it, we
231+
/// prevent the intermediate steps from occurring again, at least in the exact same way.
232+
/// However, the statistics collected for `prioritize` may want to analyze those intermediate
233+
/// steps. For example we might start with "there is no version 1 of Z", and
234+
/// `conflict_resolution` may be able to determine that "that was inevitable when we picked
235+
/// version 1 of X" which was inevitable when we picked W and so on, until version 1 of B, which
236+
/// was depended on by version 1 of A. Therefore the root cause may simplify all the way down to
237+
/// "we cannot pick version 1 of A". This will prevent us going down this path again. However
238+
/// when we start looking at version 2 of A, and discover that it depends on version 2 of B, we
239+
/// will want to prioritize the chain of intermediate steps to check if it has a problem with
240+
/// the same shape. The `satisfier_causes` argument keeps track of these intermediate steps so
241+
/// that the caller can use them for prioritization.
215242
#[allow(clippy::type_complexity)]
216243
#[cold]
217244
fn conflict_resolution(
218245
&mut self,
219246
incompatibility: IncompDpId<DP>,
247+
satisfier_causes: &mut SmallVec<(Id<DP::P>, IncompDpId<DP>)>,
220248
) -> Result<(Id<DP::P>, IncompDpId<DP>), IncompDpId<DP>> {
221249
let mut current_incompat_id = incompatibility;
222250
let mut current_incompat_changed = false;
@@ -240,6 +268,7 @@ impl<DP: DependencyProvider> State<DP> {
240268
previous_satisfier_level,
241269
);
242270
log::info!("backtrack to {:?}", previous_satisfier_level);
271+
satisfier_causes.push((package, current_incompat_id));
243272
return Ok((package, current_incompat_id));
244273
}
245274
SatisfierSearch::SameDecisionLevels { satisfier_cause } => {
@@ -251,6 +280,7 @@ impl<DP: DependencyProvider> State<DP> {
251280
);
252281
log::info!("prior cause: {}", prior_cause.display(&self.package_store));
253282
current_incompat_id = self.incompatibility_store.alloc(prior_cause);
283+
satisfier_causes.push((package, current_incompat_id));
254284
current_incompat_changed = true;
255285
}
256286
}

src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
//! and [SemanticVersion] for versions.
7272
//! This may be done quite easily by implementing the three following functions.
7373
//! ```
74-
//! # use pubgrub::{DependencyProvider, Dependencies, SemanticVersion, Ranges, DependencyConstraints, Map, PackageResolutionStatistics};
74+
//! # use pubgrub::{DependencyProvider, Dependencies, SemanticVersion, Ranges,
75+
//! # DependencyConstraints, Map, PackageResolutionStatistics};
7576
//! # use std::error::Error;
7677
//! # use std::borrow::Borrow;
7778
//! # use std::convert::Infallible;

src/provider.rs

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ use std::cmp::Reverse;
22
use std::collections::BTreeMap;
33
use std::convert::Infallible;
44

5-
use crate::solver::PackageResolutionStatistics;
6-
use crate::{Dependencies, DependencyConstraints, DependencyProvider, Map, Package, VersionSet};
5+
use crate::{
6+
Dependencies, DependencyConstraints, DependencyProvider, Map, Package,
7+
PackageResolutionStatistics, VersionSet,
8+
};
79

810
/// A basic implementation of [DependencyProvider].
911
#[derive(Debug, Clone, Default)]
@@ -102,15 +104,15 @@ impl<P: Package, VS: VersionSet> DependencyProvider for OfflineDependencyProvide
102104
range: &Self::VS,
103105
package_statistics: &PackageResolutionStatistics,
104106
) -> Self::Priority {
105-
(
106-
package_statistics.conflict_count(),
107-
Reverse(
108-
self.dependencies
109-
.get(package)
110-
.map(|versions| versions.keys().filter(|v| range.contains(v)).count())
111-
.unwrap_or(0),
112-
),
113-
)
107+
let version_count = self
108+
.dependencies
109+
.get(package)
110+
.map(|versions| versions.keys().filter(|v| range.contains(v)).count())
111+
.unwrap_or(0);
112+
if version_count == 0 {
113+
return (u32::MAX, Reverse(0));
114+
}
115+
(package_statistics.conflict_count(), Reverse(version_count))
114116
}
115117

116118
#[inline]

src/solver.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@ pub fn resolve<DP: DependencyProvider>(
126126
"unit_propagation: {:?} = '{}'",
127127
&next, state.package_store[next]
128128
);
129-
let root_causes = state.unit_propagation(next)?;
130-
for (affected, incompat) in root_causes {
129+
let satisfier_causes = state.unit_propagation(next)?;
130+
for (affected, incompat) in satisfier_causes {
131131
conflict_tracker
132132
.entry(affected)
133133
.or_default()

0 commit comments

Comments
 (0)