Skip to content

Commit d97491e

Browse files
Eh2406mpizenberg
andauthored
feat: merge dependencies for better error messages (pubgrub-rs#163)
* bad error test * feat: merge direct dependencies * separate function for clearer documentation * Update doc comments for merge_dependency * Rename merge_dependency into merge_dependants * Use american english dependent instead of dependant * Rename dependencies into merged_dependencies * Typo mergeed -> merged --------- Co-authored-by: Matthieu Pizenberg <matthieu.pizenberg@gmail.com>
1 parent 28c63ce commit d97491e

File tree

5 files changed

+134
-15
lines changed

5 files changed

+134
-15
lines changed

src/internal/core.rs

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ pub struct State<P: Package, VS: VersionSet, Priority: Ord + Clone> {
3131
/// and will stay that way until the next conflict and backtrack is operated.
3232
contradicted_incompatibilities: rustc_hash::FxHashSet<IncompId<P, VS>>,
3333

34+
/// All incompatibilities expressing dependencies,
35+
/// with common dependents merged.
36+
merged_dependencies: Map<(P, P), SmallVec<IncompId<P, VS>>>,
37+
3438
/// Partial solution.
3539
/// TODO: remove pub.
3640
pub partial_solution: PartialSolution<P, VS, Priority>,
@@ -62,6 +66,7 @@ impl<P: Package, VS: VersionSet, Priority: Ord + Clone> State<P, VS, Priority> {
6266
partial_solution: PartialSolution::empty(),
6367
incompatibility_store,
6468
unit_propagation_buffer: SmallVec::Empty,
69+
merged_dependencies: Map::default(),
6570
}
6671
}
6772

@@ -79,11 +84,15 @@ impl<P: Package, VS: VersionSet, Priority: Ord + Clone> State<P, VS, Priority> {
7984
deps: &DependencyConstraints<P, VS>,
8085
) -> std::ops::Range<IncompId<P, VS>> {
8186
// Create incompatibilities and allocate them in the store.
82-
let new_incompats_id_range = self
83-
.incompatibility_store
84-
.alloc_iter(deps.iter().map(|dep| {
85-
Incompatibility::from_dependency(package.clone(), version.clone(), dep)
86-
}));
87+
let new_incompats_id_range =
88+
self.incompatibility_store
89+
.alloc_iter(deps.iter().map(|dep| {
90+
Incompatibility::from_dependency(
91+
package.clone(),
92+
VS::singleton(version.clone()),
93+
dep,
94+
)
95+
}));
8796
// Merge the newly created incompatibilities with the older ones.
8897
for id in IncompId::range_to_iter(new_incompats_id_range.clone()) {
8998
self.merge_incompatibility(id);
@@ -232,11 +241,31 @@ impl<P: Package, VS: VersionSet, Priority: Ord + Clone> State<P, VS, Priority> {
232241
/// (provided that no other version of foo exists between 1.0.0 and 2.0.0).
233242
/// We could collapse them into { foo (1.0.0 ∪ 1.1.0), not bar ^1.0.0 }
234243
/// without having to check the existence of other versions though.
235-
///
236-
/// Here we do the simple stupid thing of just growing the Vec.
237-
/// It may not be trivial since those incompatibilities
238-
/// may already have derived others.
239-
fn merge_incompatibility(&mut self, id: IncompId<P, VS>) {
244+
fn merge_incompatibility(&mut self, mut id: IncompId<P, VS>) {
245+
if let Some((p1, p2)) = self.incompatibility_store[id].as_dependency() {
246+
// If we are a dependency, there's a good chance we can be merged with a previous dependency
247+
let deps_lookup = self
248+
.merged_dependencies
249+
.entry((p1.clone(), p2.clone()))
250+
.or_default();
251+
if let Some((past, merged)) = deps_lookup.as_mut_slice().iter_mut().find_map(|past| {
252+
self.incompatibility_store[id]
253+
.merge_dependents(&self.incompatibility_store[*past])
254+
.map(|m| (past, m))
255+
}) {
256+
let new = self.incompatibility_store.alloc(merged);
257+
for (pkg, _) in self.incompatibility_store[new].iter() {
258+
self.incompatibilities
259+
.entry(pkg.clone())
260+
.or_default()
261+
.retain(|id| id != past);
262+
}
263+
*past = new;
264+
id = new;
265+
} else {
266+
deps_lookup.push(id);
267+
}
268+
}
240269
for (pkg, term) in self.incompatibility_store[id].iter() {
241270
if cfg!(debug_assertions) {
242271
assert_ne!(term, &crate::term::Term::any());

src/internal/incompatibility.rs

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,22 +107,63 @@ impl<P: Package, VS: VersionSet> Incompatibility<P, VS> {
107107
}
108108

109109
/// Build an incompatibility from a given dependency.
110-
pub fn from_dependency(package: P, version: VS::V, dep: (&P, &VS)) -> Self {
111-
let set1 = VS::singleton(version);
110+
pub fn from_dependency(package: P, versions: VS, dep: (&P, &VS)) -> Self {
112111
let (p2, set2) = dep;
113112
Self {
114113
package_terms: if set2 == &VS::empty() {
115-
SmallMap::One([(package.clone(), Term::Positive(set1.clone()))])
114+
SmallMap::One([(package.clone(), Term::Positive(versions.clone()))])
116115
} else {
117116
SmallMap::Two([
118-
(package.clone(), Term::Positive(set1.clone())),
117+
(package.clone(), Term::Positive(versions.clone())),
119118
(p2.clone(), Term::Negative(set2.clone())),
120119
])
121120
},
122-
kind: Kind::FromDependencyOf(package, set1, p2.clone(), set2.clone()),
121+
kind: Kind::FromDependencyOf(package, versions, p2.clone(), set2.clone()),
123122
}
124123
}
125124

125+
pub fn as_dependency(&self) -> Option<(&P, &P)> {
126+
match &self.kind {
127+
Kind::FromDependencyOf(p1, _, p2, _) => Some((p1, p2)),
128+
_ => None,
129+
}
130+
}
131+
132+
/// Merge dependant versions with the same dependency.
133+
///
134+
/// When multiple versions of a package depend on the same range of another package,
135+
/// we can merge the two into a single incompatibility.
136+
/// For example, if a@1 depends on b and a@2 depends on b, we can say instead
137+
/// a@1 and a@b depend on b.
138+
///
139+
/// It is a special case of prior cause computation where the unified package
140+
/// is the common dependant in the two incompatibilities expressing dependencies.
141+
pub fn merge_dependents(&self, other: &Self) -> Option<Self> {
142+
// It is almost certainly a bug to call this method without checking that self is a dependency
143+
debug_assert!(self.as_dependency().is_some());
144+
// Check that both incompatibilities are of the shape p1 depends on p2,
145+
// with the same p1 and p2.
146+
let self_pkgs = self.as_dependency()?;
147+
if self_pkgs != other.as_dependency()? {
148+
return None;
149+
}
150+
let (p1, p2) = self_pkgs;
151+
let dep_term = self.get(p2);
152+
// The dependency range for p2 must be the same in both case
153+
// to be able to merge multiple p1 ranges.
154+
if dep_term != other.get(p2) {
155+
return None;
156+
}
157+
return Some(Self::from_dependency(
158+
p1.clone(),
159+
self.get(p1)
160+
.unwrap()
161+
.unwrap_positive()
162+
.union(other.get(p1).unwrap().unwrap_positive()), // It is safe to `simplify` here
163+
(&p2, dep_term.map_or(&VS::empty(), |v| v.unwrap_negative())),
164+
));
165+
}
166+
126167
/// Prior cause of two incompatibilities using the rule of resolution.
127168
pub fn prior_cause(
128169
incompat: Id<Self>,

src/internal/small_vec.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@ impl<T> SmallVec<T> {
2828
}
2929
}
3030

31+
pub fn as_mut_slice(&mut self) -> &mut [T] {
32+
match self {
33+
Self::Empty => &mut [],
34+
Self::One(v) => v,
35+
Self::Two(v) => v,
36+
Self::Flexible(v) => v,
37+
}
38+
}
39+
3140
pub fn push(&mut self, new: T) {
3241
*self = match std::mem::take(self) {
3342
Self::Empty => Self::One([new]),

src/term.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,15 @@ impl<VS: VersionSet> Term<VS> {
7070
_ => panic!("Negative term cannot unwrap positive set"),
7171
}
7272
}
73+
74+
/// Unwrap the set contained in a negative term.
75+
/// Will panic if used on a positive set.
76+
pub(crate) fn unwrap_negative(&self) -> &VS {
77+
match self {
78+
Self::Negative(set) => set,
79+
_ => panic!("Positive term cannot unwrap negative set"),
80+
}
81+
}
7382
}
7483

7584
/// Set operations with terms.

tests/examples.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// SPDX-License-Identifier: MPL-2.0
22

3+
use pubgrub::error::PubGrubError;
34
use pubgrub::range::Range;
5+
use pubgrub::report::{DefaultStringReporter, Reporter as _};
46
use pubgrub::solver::{resolve, OfflineDependencyProvider};
57
use pubgrub::type_aliases::Map;
68
use pubgrub::version::{NumberVersion, SemanticVersion};
@@ -209,3 +211,32 @@ fn double_choices() {
209211
let computed_solution = resolve(&dependency_provider, "a", 0).unwrap();
210212
assert_eq!(expected_solution, computed_solution);
211213
}
214+
215+
#[test]
216+
fn confusing_with_lots_of_holes() {
217+
let mut dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new();
218+
219+
// root depends on foo...
220+
dependency_provider.add_dependencies("root", 1, vec![("foo", Range::full())]);
221+
222+
for i in 1..6 {
223+
// foo depends on bar...
224+
dependency_provider.add_dependencies("foo", i as u32, vec![("bar", Range::full())]);
225+
}
226+
227+
let Err(PubGrubError::NoSolution(mut derivation_tree)) =
228+
resolve(&dependency_provider, "root", 1)
229+
else {
230+
unreachable!()
231+
};
232+
assert_eq!(
233+
&DefaultStringReporter::report(&derivation_tree),
234+
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.
235+
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."#
236+
);
237+
derivation_tree.collapse_no_versions();
238+
assert_eq!(
239+
&DefaultStringReporter::report(&derivation_tree),
240+
"Because foo depends on bar and root 1 depends on foo, root 1 is forbidden."
241+
);
242+
}

0 commit comments

Comments
 (0)