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 3 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
47 changes: 37 additions & 10 deletions src/internal/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ 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>>,

dependencies: Map<(P, P), SmallVec<IncompId<P, VS>>>,
Copy link
Member

@mpizenberg mpizenberg Dec 15, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you already tested it but I'm gonna ask just in case. Considering we only merge dependants where the dependency is exactly the same, would it make sense to store the dependency in the key? It does increase memory so it's probably worse, but it would prevent from looping through all incompats for the packages pair.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose these package pairs are generally small anyway since you opted for SmallVec so yeah maybe it's a bad trade off. But still curious to hear from if you tried it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering we only merge dependants where the dependency is exactly the same, would it make sense to store the dependency in the key? It does increase memory so it's probably worse, but it would prevent from looping through all incompats for the packages pair.

I would absolutely love to do this. Unfortunately VS is neither Hash nor Ord, so I don't know of an efficient data structure for using them as a key.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, Ord would not make sense for sets. Hash could make sense. Maybe we can make a note of this, to explore in the future if it turns out some real-world use case end up being in the worst case scenario for this (many different ranges for a package pair, causing O(n²) complexity).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Realistically, this can only happen for a package pair where there are many versions of each, their versions are bumped in sync. Something like two packages from the same workspace maybe if versions are strictly specified. Or for things like features (in our "pubgrub limitations" guide section) where we pair exactly a feature version to the bare crate version. Hum, maybe not so unlikely ...

Copy link
Member Author

@Eh2406 Eh2406 Dec 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. "Features" is why none of the cargo benchmarks hit this code path.

I'm not too afraid of the O(n²) overhead here. Every time we merge dependencies we follow it up by doing unit_propagation, which does a linear scan over an even larger collection of incompatibilities. (Well... unit_propagation has "previously_contradicted" as an optimization. So theoretically speaking unit_propagation is not guaranteed to be slower, but so far it has been in practice.) That is to say it has the same O(n²) shaped, with a larger n. If this becomes a hotspot in a resolution that is slow we can investigate a fixed then.


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

Expand All @@ -78,11 +81,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 +237,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
.dependencies
.entry((p1.clone(), p2.clone()))
.or_default();
if let Some((past, mergeed)) = deps_lookup.as_mut_slice().iter_mut().find_map(|past| {
self.incompatibility_store[id]
.merge_dependency(&self.incompatibility_store[*past])
.map(|m| (past, m))
}) {
let new = self.incompatibility_store.alloc(mergeed);
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
45 changes: 40 additions & 5 deletions src/internal/incompatibility.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,22 +107,57 @@ 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 two dependencies into one incompatibility.
///
/// When there are two (or more) versions of a package that depend in the same way on a nother package,
/// both constraints can be stored in one incompatibility.
/// This can be thought of as a alternative constructor like [`Self::from_dependency`].
/// Alternatively this can be thought of as a way to combine the two incompatibilities,
/// as a specialization of [`Self::prior_cause`].
pub fn merge_dependency(&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());
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);
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."
);
}
Loading