Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: merge dependencies for better error messages #163

Merged
merged 8 commits into from
Dec 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions src/internal/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ pub struct State<P: Package, VS: VersionSet, Priority: Ord + Clone> {
/// and will stay that way until the next conflict and backtrack is operated.
contradicted_incompatibilities: rustc_hash::FxHashSet<IncompId<P, VS>>,

/// All incompatibilities expressing dependencies,
/// with common dependents merged.
merged_dependencies: Map<(P, P), SmallVec<IncompId<P, VS>>>,

/// Partial solution.
/// TODO: remove pub.
pub partial_solution: PartialSolution<P, VS, Priority>,
Expand Down Expand Up @@ -61,6 +65,7 @@ impl<P: Package, VS: VersionSet, Priority: Ord + Clone> State<P, VS, Priority> {
partial_solution: PartialSolution::empty(),
incompatibility_store,
unit_propagation_buffer: SmallVec::Empty,
merged_dependencies: Map::default(),
}
}

Expand All @@ -78,11 +83,15 @@ impl<P: Package, VS: VersionSet, Priority: Ord + Clone> State<P, VS, Priority> {
deps: &DependencyConstraints<P, VS>,
) -> std::ops::Range<IncompId<P, VS>> {
// Create incompatibilities and allocate them in the store.
let new_incompats_id_range = self
.incompatibility_store
.alloc_iter(deps.iter().map(|dep| {
Incompatibility::from_dependency(package.clone(), version.clone(), dep)
}));
let new_incompats_id_range =
self.incompatibility_store
.alloc_iter(deps.iter().map(|dep| {
Incompatibility::from_dependency(
package.clone(),
VS::singleton(version.clone()),
dep,
)
}));
// Merge the newly created incompatibilities with the older ones.
for id in IncompId::range_to_iter(new_incompats_id_range.clone()) {
self.merge_incompatibility(id);
Expand Down Expand Up @@ -230,11 +239,31 @@ impl<P: Package, VS: VersionSet, Priority: Ord + Clone> State<P, VS, Priority> {
/// (provided that no other version of foo exists between 1.0.0 and 2.0.0).
/// We could collapse them into { foo (1.0.0 ∪ 1.1.0), not bar ^1.0.0 }
/// without having to check the existence of other versions though.
///
/// Here we do the simple stupid thing of just growing the Vec.
/// It may not be trivial since those incompatibilities
/// may already have derived others.
fn merge_incompatibility(&mut self, id: IncompId<P, VS>) {
fn merge_incompatibility(&mut self, mut id: IncompId<P, VS>) {
if let Some((p1, p2)) = self.incompatibility_store[id].as_dependency() {
// If we are a dependency, there's a good chance we can be merged with a previous dependency
let deps_lookup = self
.merged_dependencies
.entry((p1.clone(), p2.clone()))
.or_default();
if let Some((past, merged)) = deps_lookup.as_mut_slice().iter_mut().find_map(|past| {
self.incompatibility_store[id]
.merge_dependents(&self.incompatibility_store[*past])
.map(|m| (past, m))
}) {
let new = self.incompatibility_store.alloc(merged);
for (pkg, _) in self.incompatibility_store[new].iter() {
self.incompatibilities
.entry(pkg.clone())
.or_default()
.retain(|id| id != past);
}
*past = new;
id = new;
} else {
deps_lookup.push(id);
}
}
for (pkg, term) in self.incompatibility_store[id].iter() {
if cfg!(debug_assertions) {
assert_ne!(term, &crate::term::Term::any());
Expand Down
51 changes: 46 additions & 5 deletions src/internal/incompatibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,63 @@ impl<P: Package, VS: VersionSet> Incompatibility<P, VS> {
}

/// Build an incompatibility from a given dependency.
pub fn from_dependency(package: P, version: VS::V, dep: (&P, &VS)) -> Self {
let set1 = VS::singleton(version);
pub fn from_dependency(package: P, versions: VS, dep: (&P, &VS)) -> Self {
let (p2, set2) = dep;
Self {
package_terms: if set2 == &VS::empty() {
SmallMap::One([(package.clone(), Term::Positive(set1.clone()))])
SmallMap::One([(package.clone(), Term::Positive(versions.clone()))])
} else {
SmallMap::Two([
(package.clone(), Term::Positive(set1.clone())),
(package.clone(), Term::Positive(versions.clone())),
(p2.clone(), Term::Negative(set2.clone())),
])
},
kind: Kind::FromDependencyOf(package, set1, p2.clone(), set2.clone()),
kind: Kind::FromDependencyOf(package, versions, p2.clone(), set2.clone()),
}
}

pub fn as_dependency(&self) -> Option<(&P, &P)> {
match &self.kind {
Kind::FromDependencyOf(p1, _, p2, _) => Some((p1, p2)),
_ => None,
}
}

/// Merge dependant versions with the same dependency.
///
/// When multiple versions of a package depend on the same range of another package,
/// we can merge the two into a single incompatibility.
/// For example, if a@1 depends on b and a@2 depends on b, we can say instead
/// a@1 and a@b depend on b.
///
/// It is a special case of prior cause computation where the unified package
/// is the common dependant in the two incompatibilities expressing dependencies.
pub fn merge_dependents(&self, other: &Self) -> Option<Self> {
// It is almost certainly a bug to call this method without checking that self is a dependency
debug_assert!(self.as_dependency().is_some());
// Check that both incompatibilities are of the shape p1 depends on p2,
// with the same p1 and p2.
let self_pkgs = self.as_dependency()?;
if self_pkgs != other.as_dependency()? {
return None;
}
let (p1, p2) = self_pkgs;
let dep_term = self.get(p2);
// The dependency range for p2 must be the same in both case
// to be able to merge multiple p1 ranges.
if dep_term != other.get(p2) {
return None;
}
return Some(Self::from_dependency(
p1.clone(),
self.get(p1)
.unwrap()
.unwrap_positive()
.union(other.get(p1).unwrap().unwrap_positive()), // It is safe to `simplify` here
(&p2, dep_term.map_or(&VS::empty(), |v| v.unwrap_negative())),
));
}

/// Prior cause of two incompatibilities using the rule of resolution.
pub fn prior_cause(
incompat: Id<Self>,
Expand Down
9 changes: 9 additions & 0 deletions src/internal/small_vec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ impl<T> SmallVec<T> {
}
}

pub fn as_mut_slice(&mut self) -> &mut [T] {
match self {
Self::Empty => &mut [],
Self::One(v) => v,
Self::Two(v) => v,
Self::Flexible(v) => v,
}
}

pub fn push(&mut self, new: T) {
*self = match std::mem::take(self) {
Self::Empty => Self::One([new]),
Expand Down
9 changes: 9 additions & 0 deletions src/term.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ impl<VS: VersionSet> Term<VS> {
_ => panic!("Negative term cannot unwrap positive set"),
}
}

/// Unwrap the set contained in a negative term.
/// Will panic if used on a positive set.
pub(crate) fn unwrap_negative(&self) -> &VS {
match self {
Self::Negative(set) => set,
_ => panic!("Positive term cannot unwrap negative set"),
}
}
}

/// Set operations with terms.
Expand Down
31 changes: 31 additions & 0 deletions tests/examples.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MPL-2.0

use pubgrub::error::PubGrubError;
use pubgrub::range::Range;
use pubgrub::report::{DefaultStringReporter, Reporter as _};
use pubgrub::solver::{resolve, OfflineDependencyProvider};
use pubgrub::type_aliases::Map;
use pubgrub::version::{NumberVersion, SemanticVersion};
Expand Down Expand Up @@ -209,3 +211,32 @@ fn double_choices() {
let computed_solution = resolve(&dependency_provider, "a", 0).unwrap();
assert_eq!(expected_solution, computed_solution);
}

#[test]
fn confusing_with_lots_of_holes() {
let mut dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new();

// root depends on foo...
dependency_provider.add_dependencies("root", 1, vec![("foo", Range::full())]);

for i in 1..6 {
// foo depends on bar...
dependency_provider.add_dependencies("foo", i as u32, vec![("bar", Range::full())]);
}

let Err(PubGrubError::NoSolution(mut derivation_tree)) =
resolve(&dependency_provider, "root", 1)
else {
unreachable!()
};
assert_eq!(
&DefaultStringReporter::report(&derivation_tree),
r#"Because there is no available version for bar and foo 1 | 2 | 3 | 4 | 5 depends on bar, foo 1 | 2 | 3 | 4 | 5 is forbidden.
And because there is no version of foo in <1 | >1, <2 | >2, <3 | >3, <4 | >4, <5 | >5 and root 1 depends on foo, root 1 is forbidden."#
);
derivation_tree.collapse_no_versions();
mpizenberg marked this conversation as resolved.
Show resolved Hide resolved
assert_eq!(
&DefaultStringReporter::report(&derivation_tree),
"Because foo depends on bar and root 1 depends on foo, root 1 is forbidden."
);
}