From 1cc5a435df870583c73184a9cb0060878623d02e Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sat, 7 Aug 2021 22:28:59 +0200 Subject: [PATCH 01/14] refactor: introduce a trait for sets of versions --- src/lib.rs | 2 + src/range.rs | 23 +++ src/term_bis.rs | 213 +++++++++++++++++++++ src/version_set.rs | 448 +++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 686 insertions(+) create mode 100644 src/term_bis.rs create mode 100644 src/version_set.rs diff --git a/src/lib.rs b/src/lib.rs index 7a6e1737..35754bb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,7 +215,9 @@ pub mod range; pub mod report; pub mod solver; pub mod term; +pub mod term_bis; pub mod type_aliases; pub mod version; +pub mod version_set; mod internal; diff --git a/src/range.rs b/src/range.rs index 8de8b3ff..ed8ecb58 100644 --- a/src/range.rs +++ b/src/range.rs @@ -19,6 +19,29 @@ use std::fmt; use crate::internal::small_vec::SmallVec; use crate::version::Version; +use crate::version_set::VersionSet; + +impl VersionSet for Range { + type V = V; + // Constructors + fn empty() -> Self { + Range::none() + } + fn singleton(v: Self::V) -> Self { + Range::exact(v) + } + // Operations + fn complement(&self) -> Self { + self.negate() + } + fn intersection(&self, other: &Self) -> Self { + self.intersection(other) + } + // Membership + fn contains(&self, v: &Self::V) -> bool { + self.contains(v) + } +} /// A Range is a set of versions. #[derive(Debug, Clone, Eq, PartialEq)] diff --git a/src/term_bis.rs b/src/term_bis.rs new file mode 100644 index 00000000..a2f4f90c --- /dev/null +++ b/src/term_bis.rs @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! A term is the fundamental unit of operation of the PubGrub algorithm. +//! It is a positive or negative expression regarding a set of versions. + +use crate::version_set::VersionSet; +use std::fmt::{self, Display}; + +/// A positive or negative expression regarding a set of versions. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Term { + /// For example, "1.0.0 <= v < 2.0.0" is a positive expression + /// that is evaluated true if a version is selected + /// and comprised between version 1.0.0 and version 2.0.0. + Positive(VS), + /// The term "not v < 3.0.0" is a negative expression + /// that is evaluated true if a version is selected >= 3.0.0 + /// or if no version is selected at all. + Negative(VS), +} + +/// Base methods. +impl Term { + /// A term that is always true. + pub(crate) fn any() -> Self { + Self::Negative(VS::empty()) + } + + /// A term that is never true. + pub(crate) fn empty() -> Self { + Self::Positive(VS::empty()) + } + + /// A positive term containing exactly that version. + pub(crate) fn exact(version: VS::V) -> Self { + Self::Positive(VS::singleton(version)) + } + + /// Simply check if a term is positive. + pub(crate) fn is_positive(&self) -> bool { + match self { + Self::Positive(_) => true, + Self::Negative(_) => false, + } + } + + /// Negate a term. + /// Evaluation of a negated term always returns + /// the opposite of the evaluation of the original one. + pub(crate) fn negate(&self) -> Self { + match self { + Self::Positive(set) => Self::Negative(set.clone()), + Self::Negative(set) => Self::Positive(set.clone()), + } + } + + /// Evaluate a term regarding a given choice of version. + pub(crate) fn contains(&self, v: &VS::V) -> bool { + match self { + Self::Positive(set) => set.contains(v), + Self::Negative(set) => !(set.contains(v)), + } + } + + /// Unwrap the set contained in a positive term. + /// Will panic if used on a negative set. + pub(crate) fn unwrap_positive(&self) -> &VS { + match self { + Self::Positive(set) => set, + _ => panic!("Negative term cannot unwrap positive set"), + } + } +} + +/// Set operations with terms. +impl Term { + /// Compute the intersection of two terms. + /// If at least one term is positive, the intersection is also positive. + pub(crate) fn intersection(&self, other: &Self) -> Self { + match (self, other) { + (Self::Positive(r1), Self::Positive(r2)) => Self::Positive(r1.intersection(r2)), + (Self::Positive(r1), Self::Negative(r2)) => { + Self::Positive(r1.intersection(&r2.complement())) + } + (Self::Negative(r1), Self::Positive(r2)) => { + Self::Positive(r1.complement().intersection(r2)) + } + (Self::Negative(r1), Self::Negative(r2)) => Self::Negative(r1.union(r2)), + } + } + + /// Compute the union of two terms. + /// If at least one term is negative, the union is also negative. + pub(crate) fn union(&self, other: &Self) -> Self { + (self.negate().intersection(&other.negate())).negate() + } + + /// Indicate if this term is a subset of another term. + /// Just like for sets, we say that t1 is a subset of t2 + /// if and only if t1 ∩ t2 = t1. + pub(crate) fn subset_of(&self, other: &Self) -> bool { + self == &self.intersection(other) + } +} + +/// Describe a relation between a set of terms S and another term t. +/// +/// As a shorthand, we say that a term v +/// satisfies or contradicts a term t if {v} satisfies or contradicts it. +pub(crate) enum Relation { + /// We say that a set of terms S "satisfies" a term t + /// if t must be true whenever every term in S is true. + Satisfied, + /// Conversely, S "contradicts" t if t must be false + /// whenever every term in S is true. + Contradicted, + /// If neither of these is true we say that S is "inconclusive" for t. + Inconclusive, +} + +/// Relation between terms. +impl Term { + /// Check if a set of terms satisfies this term. + /// + /// We say that a set of terms S "satisfies" a term t + /// if t must be true whenever every term in S is true. + /// + /// It turns out that this can also be expressed with set operations: + /// S satisfies t if and only if ⋂ S ⊆ t + #[cfg(test)] + fn satisfied_by(&self, terms_intersection: &Self) -> bool { + terms_intersection.subset_of(self) + } + + /// Check if a set of terms contradicts this term. + /// + /// We say that a set of terms S "contradicts" a term t + /// if t must be false whenever every term in S is true. + /// + /// It turns out that this can also be expressed with set operations: + /// S contradicts t if and only if ⋂ S is disjoint with t + /// S contradicts t if and only if (⋂ S) ⋂ t = ∅ + #[cfg(test)] + fn contradicted_by(&self, terms_intersection: &Self) -> bool { + terms_intersection.intersection(self) == Self::empty() + } + + /// Check if a set of terms satisfies or contradicts a given term. + /// Otherwise the relation is inconclusive. + pub(crate) fn relation_with(&self, other_terms_intersection: &Self) -> Relation { + let full_intersection = self.intersection(other_terms_intersection); + if &full_intersection == other_terms_intersection { + Relation::Satisfied + } else if full_intersection == Self::empty() { + Relation::Contradicted + } else { + Relation::Inconclusive + } + } +} + +// impl AsRef> for Term { +impl AsRef for Term { + fn as_ref(&self) -> &Self { + self + } +} + +// REPORT ###################################################################### + +impl Display for Term { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Positive(set) => write!(f, "{}", set), + Self::Negative(set) => write!(f, "Not ( {} )", set), + } + } +} + +// TESTS ####################################################################### + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::range::Range; + use crate::version::NumberVersion; + use proptest::prelude::*; + + pub fn strategy() -> impl Strategy>> { + prop_oneof![ + crate::range::tests::strategy().prop_map(Term::Positive), + crate::range::tests::strategy().prop_map(Term::Negative), + ] + } + + proptest! { + + // Testing relation -------------------------------- + + #[test] + fn relation_with(term1 in strategy(), term2 in strategy()) { + match term1.relation_with(&term2) { + Relation::Satisfied => assert!(term1.satisfied_by(&term2)), + Relation::Contradicted => assert!(term1.contradicted_by(&term2)), + Relation::Inconclusive => { + assert!(!term1.satisfied_by(&term2)); + assert!(!term1.contradicted_by(&term2)); + } + } + } + + } +} diff --git a/src/version_set.rs b/src/version_set.rs new file mode 100644 index 00000000..bdaeb2c5 --- /dev/null +++ b/src/version_set.rs @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! Ranges are constraints defining sets of versions. +//! +//! Concretely, those constraints correspond to any set of versions +//! representable as the concatenation, union, and complement +//! of the ranges building blocks. +//! +//! Those building blocks are: +//! - [none()](Range::none): the empty set +//! - [any()](Range::any): the set of all possible versions +//! - [exact(v)](Range::exact): the set containing only the version v +//! - [higher_than(v)](Range::higher_than): the set defined by `v <= versions` +//! - [strictly_lower_than(v)](Range::strictly_lower_than): the set defined by `versions < v` +//! - [between(v1, v2)](Range::between): the set defined by `v1 <= versions < v2` + +use std::cmp::Ordering; +use std::fmt::{self, Debug}; + +use crate::internal::small_vec::SmallVec; +use crate::version::Version; + +/// Trait describing sets of versions. +pub trait VersionSet: Debug + Clone + Eq { + /// Version type associated with the sets manipulated. + type V; + + // Constructors + /// Constructor for an empty set containing no version. + fn empty() -> Self; + /// Constructor for a set containing exactly one version. + fn singleton(v: Self::V) -> Self; + + // Operations + /// Compute the complement of this set. + fn complement(&self) -> Self; + /// Compute the intersection with another set. + fn intersection(&self, other: &Self) -> Self; + + // Membership + /// Evaluate membership of a version in this set. + fn contains(&self, v: &Self::V) -> bool; + + // Automatically implemented functions ########################### + + /// Constructor for the set containing all versions. + /// Automatically implemented as `Self::empty().complement()`. + fn full() -> Self { + Self::empty().complement() + } + + /// Compute the union with another set. + /// Thanks to set properties, this is automatically implemented as: + /// `self.complement().intersection(&other.complement()).complement()` + fn union(&self, other: &Self) -> Self { + self.complement() + .intersection(&other.complement()) + .complement() + } +} + +/// A Range is a set of versions. +#[derive(Debug, Clone, Eq, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct Range { + segments: SmallVec>, +} + +type Interval = (V, Option); + +// Range building blocks. +impl Range { + /// Empty set of versions. + pub fn none() -> Self { + Self { + segments: SmallVec::empty(), + } + } + + /// Set of all possible versions. + pub fn any() -> Self { + Self::higher_than(V::lowest()) + } + + /// Set containing exactly one version. + pub fn exact(v: impl Into) -> Self { + let v = v.into(); + Self { + segments: SmallVec::one((v.clone(), Some(v.bump()))), + } + } + + /// Set of all versions higher or equal to some version. + pub fn higher_than(v: impl Into) -> Self { + Self { + segments: SmallVec::one((v.into(), None)), + } + } + + /// Set of all versions strictly lower than some version. + pub fn strictly_lower_than(v: impl Into) -> Self { + let v = v.into(); + if v == V::lowest() { + Self::none() + } else { + Self { + segments: SmallVec::one((V::lowest(), Some(v))), + } + } + } + + /// Set of all versions comprised between two given versions. + /// The lower bound is included and the higher bound excluded. + /// `v1 <= v < v2`. + pub fn between(v1: impl Into, v2: impl Into) -> Self { + let v1 = v1.into(); + let v2 = v2.into(); + if v1 < v2 { + Self { + segments: SmallVec::one((v1, Some(v2))), + } + } else { + Self::none() + } + } +} + +// Set operations. +impl Range { + // Negate ################################################################## + + /// Compute the complement set of versions. + pub fn negate(&self) -> Self { + match self.segments.first() { + None => Self::any(), // Complement of ∅ is * + + // First high bound is +∞ + Some((v, None)) => { + // Complement of * is ∅ + if v == &V::lowest() { + Self::none() + // Complement of "v <= _" is "_ < v" + } else { + Self::strictly_lower_than(v.clone()) + } + } + + // First high bound is not +∞ + Some((v1, Some(v2))) => { + if v1 == &V::lowest() { + Self::negate_segments(v2.clone(), &self.segments[1..]) + } else { + Self::negate_segments(V::lowest(), &self.segments) + } + } + } + } + + /// Helper function performing the negation of intervals in segments. + /// For example: + /// [ (v1, None) ] => [ (start, Some(v1)) ] + /// [ (v1, Some(v2)) ] => [ (start, Some(v1)), (v2, None) ] + fn negate_segments(start: V, segments: &[Interval]) -> Range { + let mut complement_segments = SmallVec::empty(); + let mut start = Some(start); + for (v1, maybe_v2) in segments { + // start.unwrap() is fine because `segments` is not exposed, + // and our usage guaranties that only the last segment may contain a None. + complement_segments.push((start.unwrap(), Some(v1.to_owned()))); + start = maybe_v2.to_owned(); + } + if let Some(last) = start { + complement_segments.push((last, None)); + } + + Self { + segments: complement_segments, + } + } + + // Union and intersection ################################################## + + /// Compute the union of two sets of versions. + pub fn union(&self, other: &Self) -> Self { + self.negate().intersection(&other.negate()).negate() + } + + /// Compute the intersection of two sets of versions. + pub fn intersection(&self, other: &Self) -> Self { + let mut segments = SmallVec::empty(); + let mut left_iter = self.segments.iter(); + let mut right_iter = other.segments.iter(); + let mut left = left_iter.next(); + let mut right = right_iter.next(); + loop { + match (left, right) { + // Both left and right still contain a finite interval: + (Some((l1, Some(l2))), Some((r1, Some(r2)))) => { + if l2 <= r1 { + // Intervals are disjoint, progress on the left. + left = left_iter.next(); + } else if r2 <= l1 { + // Intervals are disjoint, progress on the right. + right = right_iter.next(); + } else { + // Intervals are not disjoint. + let start = l1.max(r1).to_owned(); + if l2 < r2 { + segments.push((start, Some(l2.to_owned()))); + left = left_iter.next(); + } else { + segments.push((start, Some(r2.to_owned()))); + right = right_iter.next(); + } + } + } + + // Right contains an infinite interval: + (Some((l1, Some(l2))), Some((r1, None))) => match l2.cmp(r1) { + Ordering::Less => { + left = left_iter.next(); + } + Ordering::Equal => { + for l in left_iter.cloned() { + segments.push(l) + } + break; + } + Ordering::Greater => { + let start = l1.max(r1).to_owned(); + segments.push((start, Some(l2.to_owned()))); + for l in left_iter.cloned() { + segments.push(l) + } + break; + } + }, + + // Left contains an infinite interval: + (Some((l1, None)), Some((r1, Some(r2)))) => match r2.cmp(l1) { + Ordering::Less => { + right = right_iter.next(); + } + Ordering::Equal => { + for r in right_iter.cloned() { + segments.push(r) + } + break; + } + Ordering::Greater => { + let start = l1.max(r1).to_owned(); + segments.push((start, Some(r2.to_owned()))); + for r in right_iter.cloned() { + segments.push(r) + } + break; + } + }, + + // Both sides contain an infinite interval: + (Some((l1, None)), Some((r1, None))) => { + let start = l1.max(r1).to_owned(); + segments.push((start, None)); + break; + } + + // Left or right has ended. + _ => { + break; + } + } + } + + Self { segments } + } +} + +// Other useful functions. +impl Range { + /// Check if a range contains a given version. + pub fn contains(&self, version: &V) -> bool { + for (v1, maybe_v2) in &self.segments { + match maybe_v2 { + None => return v1 <= version, + Some(v2) => { + if version < v1 { + return false; + } else if version < v2 { + return true; + } + } + } + } + false + } + + /// Return the lowest version in the range (if there is one). + pub fn lowest_version(&self) -> Option { + self.segments.first().map(|(start, _)| start).cloned() + } +} + +// REPORT ###################################################################### + +impl fmt::Display for Range { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.segments.as_slice() { + [] => write!(f, "∅"), + [(start, None)] if start == &V::lowest() => write!(f, "∗"), + [(start, None)] => write!(f, "{} <= v", start), + [(start, Some(end))] if end == &start.bump() => write!(f, "{}", start), + [(start, Some(end))] if start == &V::lowest() => write!(f, "v < {}", end), + [(start, Some(end))] => write!(f, "{} <= v < {}", start, end), + more_than_one_interval => { + let string_intervals: Vec<_> = more_than_one_interval + .iter() + .map(interval_to_string) + .collect(); + write!(f, "{}", string_intervals.join(" ")) + } + } + } +} + +fn interval_to_string((start, maybe_end): &Interval) -> String { + match maybe_end { + Some(end) => format!("[ {}, {} [", start, end), + None => format!("[ {}, ∞ [", start), + } +} + +// TESTS ####################################################################### + +#[cfg(test)] +pub mod tests { + use proptest::prelude::*; + + use crate::version::NumberVersion; + + use super::*; + + pub fn strategy() -> impl Strategy> { + prop::collection::vec(any::(), 0..10).prop_map(|mut vec| { + vec.sort_unstable(); + vec.dedup(); + let mut pair_iter = vec.chunks_exact(2); + let mut segments = SmallVec::empty(); + while let Some([v1, v2]) = pair_iter.next() { + segments.push((NumberVersion(*v1), Some(NumberVersion(*v2)))); + } + if let [v] = pair_iter.remainder() { + segments.push((NumberVersion(*v), None)); + } + Range { segments } + }) + } + + fn version_strat() -> impl Strategy { + any::().prop_map(NumberVersion) + } + + proptest! { + + // Testing negate ---------------------------------- + + #[test] + fn negate_is_different(range in strategy()) { + assert_ne!(range.negate(), range); + } + + #[test] + fn double_negate_is_identity(range in strategy()) { + assert_eq!(range.negate().negate(), range); + } + + #[test] + fn negate_contains_opposite(range in strategy(), version in version_strat()) { + assert_ne!(range.contains(&version), range.negate().contains(&version)); + } + + // Testing intersection ---------------------------- + + #[test] + fn intersection_is_symmetric(r1 in strategy(), r2 in strategy()) { + assert_eq!(r1.intersection(&r2), r2.intersection(&r1)); + } + + #[test] + fn intersection_with_any_is_identity(range in strategy()) { + assert_eq!(Range::any().intersection(&range), range); + } + + #[test] + fn intersection_with_none_is_none(range in strategy()) { + assert_eq!(Range::none().intersection(&range), Range::none()); + } + + #[test] + fn intersection_is_idempotent(r1 in strategy(), r2 in strategy()) { + assert_eq!(r1.intersection(&r2).intersection(&r2), r1.intersection(&r2)); + } + + #[test] + fn intersection_is_associative(r1 in strategy(), r2 in strategy(), r3 in strategy()) { + assert_eq!(r1.intersection(&r2).intersection(&r3), r1.intersection(&r2.intersection(&r3))); + } + + #[test] + fn intesection_of_complements_is_none(range in strategy()) { + assert_eq!(range.negate().intersection(&range), Range::none()); + } + + #[test] + fn intesection_contains_both(r1 in strategy(), r2 in strategy(), version in version_strat()) { + assert_eq!(r1.intersection(&r2).contains(&version), r1.contains(&version) && r2.contains(&version)); + } + + // Testing union ----------------------------------- + + #[test] + fn union_of_complements_is_any(range in strategy()) { + assert_eq!(range.negate().union(&range), Range::any()); + } + + #[test] + fn union_contains_either(r1 in strategy(), r2 in strategy(), version in version_strat()) { + assert_eq!(r1.union(&r2).contains(&version), r1.contains(&version) || r2.contains(&version)); + } + + // Testing contains -------------------------------- + + #[test] + fn always_contains_exact(version in version_strat()) { + assert!(Range::exact(version).contains(&version)); + } + + #[test] + fn contains_negation(range in strategy(), version in version_strat()) { + assert_ne!(range.contains(&version), range.negate().contains(&version)); + } + + #[test] + fn contains_intersection(range in strategy(), version in version_strat()) { + assert_eq!(range.contains(&version), range.intersection(&Range::exact(version)) != Range::none()); + } + } +} From f8923d89cf6d302a3e20b82be8935457255e6e91 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sat, 7 Aug 2021 23:08:47 +0200 Subject: [PATCH 02/14] refactor: port report and incompatibility to version set --- src/internal/incompatibility_bis.rs | 296 +++++++++++++++++ src/internal/mod.rs | 1 + src/lib.rs | 1 + src/report_bis.rs | 481 ++++++++++++++++++++++++++++ src/version_set.rs | 398 +---------------------- 5 files changed, 782 insertions(+), 395 deletions(-) create mode 100644 src/internal/incompatibility_bis.rs create mode 100644 src/report_bis.rs diff --git a/src/internal/incompatibility_bis.rs b/src/internal/incompatibility_bis.rs new file mode 100644 index 00000000..03f67b22 --- /dev/null +++ b/src/internal/incompatibility_bis.rs @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! An incompatibility is a set of terms for different packages +//! that should never be satisfied all together. + +use std::collections::HashSet as Set; +use std::fmt; + +use crate::internal::arena::{Arena, Id}; +use crate::internal::small_map::SmallMap; +use crate::package::Package; +use crate::report_bis::{DefaultStringReporter, DerivationTree, Derived, External}; +use crate::term_bis::{self, Term}; +use crate::version_set::VersionSet; + +/// An incompatibility is a set of terms for different packages +/// that should never be satisfied all together. +/// An incompatibility usually originates from a package dependency. +/// For example, if package A at version 1 depends on package B +/// at version 2, you can never have both terms `A = 1` +/// and `not B = 2` satisfied at the same time in a partial solution. +/// This would mean that we found a solution with package A at version 1 +/// but not with package B at version 2. +/// Yet A at version 1 depends on B at version 2 so this is not possible. +/// Therefore, the set `{ A = 1, not B = 2 }` is an incompatibility, +/// defined from dependencies of A at version 1. +/// +/// Incompatibilities can also be derived from two other incompatibilities +/// during conflict resolution. More about all this in +/// [PubGrub documentation](https://github.com/dart-lang/pub/blob/master/doc/solver.md#incompatibility). +#[derive(Debug, Clone)] +pub struct Incompatibility { + package_terms: SmallMap>, + kind: Kind, +} + +/// Type alias of unique identifiers for incompatibilities. +pub type IncompId = Id>; + +#[derive(Debug, Clone)] +enum Kind { + /// Initial incompatibility aiming at picking the root package for the first decision. + NotRoot(P, VS::V), + /// There are no versions in the given range for this package. + NoVersions(P, VS), + /// Dependencies of the package are unavailable for versions in that range. + UnavailableDependencies(P, VS), + /// Incompatibility coming from the dependencies of a given package. + FromDependencyOf(P, VS, P, VS), + /// Derived from two causes. Stores cause ids. + DerivedFrom(IncompId, IncompId), +} + +/// A Relation describes how a set of terms can be compared to an incompatibility. +/// Typically, the set of terms comes from the partial solution. +#[derive(Eq, PartialEq, Debug)] +pub enum Relation { + /// We say that a set of terms S satisfies an incompatibility I + /// if S satisfies every term in I. + Satisfied, + /// We say that S contradicts I + /// if S contradicts at least one term in I. + Contradicted(P), + /// If S satisfies all but one of I's terms and is inconclusive for the remaining term, + /// we say S "almost satisfies" I and we call the remaining term the "unsatisfied term". + AlmostSatisfied(P), + /// Otherwise, we say that their relation is inconclusive. + Inconclusive, +} + +impl Incompatibility { + /// Create the initial "not Root" incompatibility. + pub fn not_root(package: P, version: VS::V) -> Self { + Self { + package_terms: SmallMap::One([( + package.clone(), + Term::Negative(VS::singleton(version.clone())), + )]), + kind: Kind::NotRoot(package, version), + } + } + + /// Create an incompatibility to remember + /// that a given set does not contain any version. + pub fn no_versions(package: P, term: Term) -> Self { + let set = match &term { + Term::Positive(r) => r.clone(), + Term::Negative(_) => panic!("No version should have a positive term"), + }; + Self { + package_terms: SmallMap::One([(package.clone(), term)]), + kind: Kind::NoVersions(package, set), + } + } + + /// Create an incompatibility to remember + /// that a package version is not selectable + /// because its list of dependencies is unavailable. + pub fn unavailable_dependencies(package: P, version: VS::V) -> Self { + let set = VS::singleton(version); + Self { + package_terms: SmallMap::One([(package.clone(), Term::Positive(set.clone()))]), + kind: Kind::UnavailableDependencies(package, set), + } + } + + /// 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); + let (p2, set2) = dep; + Self { + package_terms: SmallMap::Two([ + (package.clone(), Term::Positive(set1.clone())), + (p2.clone(), Term::Negative(set2.clone())), + ]), + kind: Kind::FromDependencyOf(package, set1, p2.clone(), set2.clone()), + } + } + + /// Prior cause of two incompatibilities using the rule of resolution. + pub fn prior_cause( + incompat: Id, + satisfier_cause: Id, + package: &P, + incompatibility_store: &Arena, + ) -> Self { + let kind = Kind::DerivedFrom(incompat, satisfier_cause); + let mut package_terms = incompatibility_store[incompat].package_terms.clone(); + let t1 = package_terms.remove(package).unwrap(); + let satisfier_cause_terms = &incompatibility_store[satisfier_cause].package_terms; + package_terms.merge( + satisfier_cause_terms.iter().filter(|(p, _)| p != &package), + |t1, t2| Some(t1.intersection(t2)), + ); + let term = t1.union(satisfier_cause_terms.get(package).unwrap()); + if term != Term::any() { + package_terms.insert(package.clone(), term); + } + Self { + package_terms, + kind, + } + } + + /// Check if an incompatibility should mark the end of the algorithm + /// because it satisfies the root package. + pub fn is_terminal(&self, root_package: &P, root_version: &VS::V) -> bool { + if self.package_terms.len() == 0 { + true + } else if self.package_terms.len() > 1 { + false + } else { + let (package, term) = self.package_terms.iter().next().unwrap(); + (package == root_package) && term.contains(&root_version) + } + } + + /// Get the term related to a given package (if it exists). + pub fn get(&self, package: &P) -> Option<&Term> { + self.package_terms.get(package) + } + + /// Iterate over packages. + pub fn iter(&self) -> impl Iterator)> { + self.package_terms.iter() + } + + // Reporting ############################################################### + + /// Retrieve parent causes if of type DerivedFrom. + pub fn causes(&self) -> Option<(Id, Id)> { + match self.kind { + Kind::DerivedFrom(id1, id2) => Some((id1, id2)), + _ => None, + } + } + + /// Build a derivation tree for error reporting. + pub fn build_derivation_tree( + self_id: Id, + shared_ids: &Set>, + store: &Arena, + ) -> DerivationTree { + match &store[self_id].kind { + Kind::DerivedFrom(id1, id2) => { + let cause1 = Self::build_derivation_tree(*id1, shared_ids, store); + let cause2 = Self::build_derivation_tree(*id2, shared_ids, store); + let derived = Derived { + terms: store[self_id].package_terms.as_map(), + shared_id: shared_ids.get(&self_id).map(|id| id.into_raw()), + cause1: Box::new(cause1), + cause2: Box::new(cause2), + }; + DerivationTree::Derived(derived) + } + Kind::NotRoot(package, version) => { + DerivationTree::External(External::NotRoot(package.clone(), version.clone())) + } + Kind::NoVersions(package, set) => { + DerivationTree::External(External::NoVersions(package.clone(), set.clone())) + } + Kind::UnavailableDependencies(package, set) => DerivationTree::External( + External::UnavailableDependencies(package.clone(), set.clone()), + ), + Kind::FromDependencyOf(package, set, dep_package, dep_set) => { + DerivationTree::External(External::FromDependencyOf( + package.clone(), + set.clone(), + dep_package.clone(), + dep_set.clone(), + )) + } + } + } +} + +impl<'a, P: Package, VS: VersionSet + 'a> Incompatibility { + /// CF definition of Relation enum. + pub fn relation(&self, terms: impl Fn(&P) -> Option<&'a Term>) -> Relation

{ + let mut relation = Relation::Satisfied; + for (package, incompat_term) in self.package_terms.iter() { + match terms(package).map(|term| incompat_term.relation_with(&term)) { + Some(term_bis::Relation::Satisfied) => {} + Some(term_bis::Relation::Contradicted) => { + return Relation::Contradicted(package.clone()); + } + None | Some(term_bis::Relation::Inconclusive) => { + // If a package is not present, the intersection is the same as [Term::any]. + // According to the rules of satisfactions, the relation would be inconclusive. + // It could also be satisfied if the incompatibility term was also [Term::any], + // but we systematically remove those from incompatibilities + // so we're safe on that front. + if relation == Relation::Satisfied { + relation = Relation::AlmostSatisfied(package.clone()); + } else { + relation = Relation::Inconclusive; + } + } + } + } + relation + } +} + +impl fmt::Display for Incompatibility { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + DefaultStringReporter::string_terms(&self.package_terms.as_map()) + ) + } +} + +// TESTS ####################################################################### + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::range::Range; + use crate::term_bis::tests::strategy as term_strat; + use crate::type_aliases::Map; + use proptest::prelude::*; + + proptest! { + + /// For any three different packages p1, p2 and p3, + /// for any three terms t1, t2 and t3, + /// if we have the two following incompatibilities: + /// { p1: t1, p2: not t2 } + /// { p2: t2, p3: t3 } + /// the rule of resolution says that we can deduce the following incompatibility: + /// { p1: t1, p3: t3 } + #[test] + fn rule_of_resolution(t1 in term_strat(), t2 in term_strat(), t3 in term_strat()) { + let mut store = Arena::new(); + let i1 = store.alloc(Incompatibility { + package_terms: SmallMap::Two([("p1", t1.clone()), ("p2", t2.negate())]), + kind: Kind::UnavailableDependencies("0", Range::any()) + }); + + let i2 = store.alloc(Incompatibility { + package_terms: SmallMap::Two([("p2", t2), ("p3", t3.clone())]), + kind: Kind::UnavailableDependencies("0", Range::any()) + }); + + let mut i3 = Map::default(); + i3.insert("p1", t1); + i3.insert("p3", t3); + + let i_resolution = Incompatibility::prior_cause(i1, i2, &"p2", &store); + assert_eq!(i_resolution.package_terms.as_map(), i3); + } + + } +} diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 86d7e22e..3f4f3c2c 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -5,6 +5,7 @@ pub mod arena; pub mod core; pub mod incompatibility; +pub mod incompatibility_bis; pub mod partial_solution; pub mod small_map; pub mod small_vec; diff --git a/src/lib.rs b/src/lib.rs index 35754bb1..5f96f43a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,6 +213,7 @@ pub mod error; pub mod package; pub mod range; pub mod report; +pub mod report_bis; pub mod solver; pub mod term; pub mod term_bis; diff --git a/src/report_bis.rs b/src/report_bis.rs new file mode 100644 index 00000000..b6cecf49 --- /dev/null +++ b/src/report_bis.rs @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! Build a report as clear as possible as to why +//! dependency solving failed. + +use std::fmt; +use std::ops::{Deref, DerefMut}; + +use crate::package::Package; +use crate::term_bis::Term; +use crate::type_aliases::Map; +use crate::version_set::VersionSet; + +/// Reporter trait. +pub trait Reporter { + /// Output type of the report. + type Output; + + /// Generate a report from the derivation tree + /// describing the resolution failure. + fn report(derivation_tree: &DerivationTree) -> Self::Output; +} + +/// Derivation tree resulting in the impossibility +/// to solve the dependencies of our root package. +#[derive(Debug, Clone)] +pub enum DerivationTree { + /// External incompatibility. + External(External), + /// Incompatibility derived from two others. + Derived(Derived), +} + +/// Incompatibilities that are not derived from others, +/// they have their own reason. +#[derive(Debug, Clone)] +pub enum External { + /// Initial incompatibility aiming at picking the root package for the first decision. + NotRoot(P, VS::V), + /// There are no versions in the given set for this package. + NoVersions(P, VS), + /// Dependencies of the package are unavailable for versions in that set. + UnavailableDependencies(P, VS), + /// Incompatibility coming from the dependencies of a given package. + FromDependencyOf(P, VS, P, VS), +} + +/// Incompatibility derived from two others. +#[derive(Debug, Clone)] +pub struct Derived { + /// Terms of the incompatibility. + pub terms: Map>, + /// Indicate if that incompatibility is present multiple times + /// in the derivation tree. + /// If that is the case, it has a unique id, provided in that option. + /// Then, we may want to only explain it once, + /// and refer to the explanation for the other times. + pub shared_id: Option, + /// First cause. + pub cause1: Box>, + /// Second cause. + pub cause2: Box>, +} + +impl DerivationTree { + /// Merge the [NoVersions](External::NoVersions) external incompatibilities + /// with the other one they are matched with + /// in a derived incompatibility. + /// This cleans up quite nicely the generated report. + /// You might want to do this if you know that the + /// [DependencyProvider](crate::solver::DependencyProvider) + /// was not run in some kind of offline mode that may not + /// have access to all versions existing. + pub fn collapse_no_versions(&mut self) { + match self { + DerivationTree::External(_) => {} + DerivationTree::Derived(derived) => { + match (derived.cause1.deref_mut(), derived.cause2.deref_mut()) { + (DerivationTree::External(External::NoVersions(p, r)), ref mut cause2) => { + cause2.collapse_no_versions(); + *self = cause2 + .clone() + .merge_no_versions(p.to_owned(), r.to_owned()) + .unwrap_or_else(|| self.to_owned()); + } + (ref mut cause1, DerivationTree::External(External::NoVersions(p, r))) => { + cause1.collapse_no_versions(); + *self = cause1 + .clone() + .merge_no_versions(p.to_owned(), r.to_owned()) + .unwrap_or_else(|| self.to_owned()); + } + _ => { + derived.cause1.collapse_no_versions(); + derived.cause2.collapse_no_versions(); + } + } + } + } + } + + fn merge_no_versions(self, package: P, set: VS) -> Option { + match self { + // TODO: take care of the Derived case. + // Once done, we can remove the Option. + DerivationTree::Derived(_) => Some(self), + DerivationTree::External(External::NotRoot(_, _)) => { + panic!("How did we end up with a NoVersions merged with a NotRoot?") + } + DerivationTree::External(External::NoVersions(_, r)) => Some(DerivationTree::External( + External::NoVersions(package, set.union(&r)), + )), + DerivationTree::External(External::UnavailableDependencies(_, r)) => Some( + DerivationTree::External(External::UnavailableDependencies(package, set.union(&r))), + ), + DerivationTree::External(External::FromDependencyOf(p1, r1, p2, r2)) => { + if p1 == package { + Some(DerivationTree::External(External::FromDependencyOf( + p1, + r1.union(&set), + p2, + r2, + ))) + } else { + Some(DerivationTree::External(External::FromDependencyOf( + p1, + r1, + p2, + r2.union(&set), + ))) + } + } + } + } +} + +impl fmt::Display for External { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NotRoot(package, version) => { + write!(f, "we are solving dependencies of {} {}", package, version) + } + Self::NoVersions(package, set) => { + if set == &VS::full() { + write!(f, "there is no available version for {}", package) + } else { + write!(f, "there is no version of {} in {}", package, set) + } + } + Self::UnavailableDependencies(package, set) => { + if set == &VS::full() { + write!(f, "dependencies of {} are unavailable", package) + } else { + write!( + f, + "dependencies of {} at version {} are unavailable", + package, set + ) + } + } + Self::FromDependencyOf(p, set_p, dep, set_dep) => { + if set_p == &VS::full() && set_dep == &VS::full() { + write!(f, "{} depends on {}", p, dep) + } else if set_p == &VS::full() { + write!(f, "{} depends on {} {}", p, dep, set_dep) + } else if set_dep == &VS::full() { + write!(f, "{} {} depends on {}", p, set_p, dep) + } else { + write!(f, "{} {} depends on {} {}", p, set_p, dep, set_dep) + } + } + } + } +} + +/// Default reporter able to generate an explanation as a [String]. +pub struct DefaultStringReporter { + /// Number of explanations already with a line reference. + ref_count: usize, + /// Shared nodes that have already been marked with a line reference. + /// The incompatibility ids are the keys, and the line references are the values. + shared_with_ref: Map, + /// Accumulated lines of the report already generated. + lines: Vec, +} + +impl DefaultStringReporter { + /// Initialize the reporter. + fn new() -> Self { + Self { + ref_count: 0, + shared_with_ref: Map::default(), + lines: Vec::new(), + } + } + + fn build_recursive(&mut self, derived: &Derived) { + self.build_recursive_helper(derived); + if let Some(id) = derived.shared_id { + if self.shared_with_ref.get(&id) == None { + self.add_line_ref(); + self.shared_with_ref.insert(id, self.ref_count); + } + }; + } + + fn build_recursive_helper(&mut self, current: &Derived) { + match (current.cause1.deref(), current.cause2.deref()) { + (DerivationTree::External(external1), DerivationTree::External(external2)) => { + // Simplest case, we just combine two external incompatibilities. + self.lines.push(Self::explain_both_external( + external1, + external2, + ¤t.terms, + )); + } + (DerivationTree::Derived(derived), DerivationTree::External(external)) => { + // One cause is derived, so we explain this first + // then we add the one-line external part + // and finally conclude with the current incompatibility. + self.report_one_each(derived, external, ¤t.terms); + } + (DerivationTree::External(external), DerivationTree::Derived(derived)) => { + self.report_one_each(derived, external, ¤t.terms); + } + (DerivationTree::Derived(derived1), DerivationTree::Derived(derived2)) => { + // This is the most complex case since both causes are also derived. + match ( + self.line_ref_of(derived1.shared_id), + self.line_ref_of(derived2.shared_id), + ) { + // If both causes already have been referenced (shared_id), + // the explanation simply uses those references. + (Some(ref1), Some(ref2)) => self.lines.push(Self::explain_both_ref( + ref1, + derived1, + ref2, + derived2, + ¤t.terms, + )), + // Otherwise, if one only has a line number reference, + // we recursively call the one without reference and then + // add the one with reference to conclude. + (Some(ref1), None) => { + self.build_recursive(derived2); + self.lines + .push(Self::and_explain_ref(ref1, derived1, ¤t.terms)); + } + (None, Some(ref2)) => { + self.build_recursive(derived1); + self.lines + .push(Self::and_explain_ref(ref2, derived2, ¤t.terms)); + } + // Finally, if no line reference exists yet, + // we call recursively the first one and then, + // - if this was a shared node, it will get a line ref + // and we can simply recall this with the current node. + // - otherwise, we add a line reference to it, + // recursively call on the second node, + // and finally conclude. + (None, None) => { + self.build_recursive(derived1); + if derived1.shared_id != None { + self.lines.push("".into()); + self.build_recursive(current); + } else { + self.add_line_ref(); + let ref1 = self.ref_count; + self.lines.push("".into()); + self.build_recursive(derived2); + self.lines + .push(Self::and_explain_ref(ref1, derived1, ¤t.terms)); + } + } + } + } + } + } + + /// Report a derived and an external incompatibility. + /// + /// The result will depend on the fact that the derived incompatibility + /// has already been explained or not. + fn report_one_each( + &mut self, + derived: &Derived, + external: &External, + current_terms: &Map>, + ) { + match self.line_ref_of(derived.shared_id) { + Some(ref_id) => self.lines.push(Self::explain_ref_and_external( + ref_id, + derived, + external, + current_terms, + )), + None => self.report_recurse_one_each(derived, external, current_terms), + } + } + + /// Report one derived (without a line ref yet) and one external. + fn report_recurse_one_each( + &mut self, + derived: &Derived, + external: &External, + current_terms: &Map>, + ) { + match (derived.cause1.deref(), derived.cause2.deref()) { + // If the derived cause has itself one external prior cause, + // we can chain the external explanations. + (DerivationTree::Derived(prior_derived), DerivationTree::External(prior_external)) => { + self.build_recursive(prior_derived); + self.lines.push(Self::and_explain_prior_and_external( + prior_external, + external, + current_terms, + )); + } + // If the derived cause has itself one external prior cause, + // we can chain the external explanations. + (DerivationTree::External(prior_external), DerivationTree::Derived(prior_derived)) => { + self.build_recursive(prior_derived); + self.lines.push(Self::and_explain_prior_and_external( + prior_external, + external, + current_terms, + )); + } + _ => { + self.build_recursive(derived); + self.lines + .push(Self::and_explain_external(external, current_terms)); + } + } + } + + // String explanations ##################################################### + + /// Simplest case, we just combine two external incompatibilities. + fn explain_both_external( + external1: &External, + external2: &External, + current_terms: &Map>, + ) -> String { + // TODO: order should be chosen to make it more logical. + format!( + "Because {} and {}, {}.", + external1, + external2, + Self::string_terms(current_terms) + ) + } + + /// Both causes have already been explained so we use their refs. + fn explain_both_ref( + ref_id1: usize, + derived1: &Derived, + ref_id2: usize, + derived2: &Derived, + current_terms: &Map>, + ) -> String { + // TODO: order should be chosen to make it more logical. + format!( + "Because {} ({}) and {} ({}), {}.", + Self::string_terms(&derived1.terms), + ref_id1, + Self::string_terms(&derived2.terms), + ref_id2, + Self::string_terms(current_terms) + ) + } + + /// One cause is derived (already explained so one-line), + /// the other is a one-line external cause, + /// and finally we conclude with the current incompatibility. + fn explain_ref_and_external( + ref_id: usize, + derived: &Derived, + external: &External, + current_terms: &Map>, + ) -> String { + // TODO: order should be chosen to make it more logical. + format!( + "Because {} ({}) and {}, {}.", + Self::string_terms(&derived.terms), + ref_id, + external, + Self::string_terms(current_terms) + ) + } + + /// Add an external cause to the chain of explanations. + fn and_explain_external( + external: &External, + current_terms: &Map>, + ) -> String { + format!( + "And because {}, {}.", + external, + Self::string_terms(current_terms) + ) + } + + /// Add an already explained incompat to the chain of explanations. + fn and_explain_ref( + ref_id: usize, + derived: &Derived, + current_terms: &Map>, + ) -> String { + format!( + "And because {} ({}), {}.", + Self::string_terms(&derived.terms), + ref_id, + Self::string_terms(current_terms) + ) + } + + /// Add an already explained incompat to the chain of explanations. + fn and_explain_prior_and_external( + prior_external: &External, + external: &External, + current_terms: &Map>, + ) -> String { + format!( + "And because {} and {}, {}.", + prior_external, + external, + Self::string_terms(current_terms) + ) + } + + /// Try to print terms of an incompatibility in a human-readable way. + pub fn string_terms(terms: &Map>) -> String { + let terms_vec: Vec<_> = terms.iter().collect(); + match terms_vec.as_slice() { + [] => "version solving failed".into(), + // TODO: special case when that unique package is root. + [(package, Term::Positive(range))] => format!("{} {} is forbidden", package, range), + [(package, Term::Negative(range))] => format!("{} {} is mandatory", package, range), + [(p1, Term::Positive(r1)), (p2, Term::Negative(r2))] => { + External::FromDependencyOf(p1, r1.clone(), p2, r2.clone()).to_string() + } + [(p1, Term::Negative(r1)), (p2, Term::Positive(r2))] => { + External::FromDependencyOf(p2, r2.clone(), p1, r1.clone()).to_string() + } + slice => { + let str_terms: Vec<_> = slice.iter().map(|(p, t)| format!("{} {}", p, t)).collect(); + str_terms.join(", ") + " are incompatible" + } + } + } + + // Helper functions ######################################################## + + fn add_line_ref(&mut self) { + let new_count = self.ref_count + 1; + self.ref_count = new_count; + if let Some(line) = self.lines.last_mut() { + *line = format!("{} ({})", line, new_count); + } + } + + fn line_ref_of(&self, shared_id: Option) -> Option { + shared_id.and_then(|id| self.shared_with_ref.get(&id).cloned()) + } +} + +impl Reporter for DefaultStringReporter { + type Output = String; + + fn report(derivation_tree: &DerivationTree) -> Self::Output { + match derivation_tree { + DerivationTree::External(external) => external.to_string(), + DerivationTree::Derived(derived) => { + let mut reporter = Self::new(); + reporter.build_recursive(derived); + reporter.lines.join("\n") + } + } + } +} diff --git a/src/version_set.rs b/src/version_set.rs index bdaeb2c5..65454e04 100644 --- a/src/version_set.rs +++ b/src/version_set.rs @@ -14,16 +14,12 @@ //! - [strictly_lower_than(v)](Range::strictly_lower_than): the set defined by `versions < v` //! - [between(v1, v2)](Range::between): the set defined by `v1 <= versions < v2` -use std::cmp::Ordering; -use std::fmt::{self, Debug}; - -use crate::internal::small_vec::SmallVec; -use crate::version::Version; +use std::fmt::{Debug, Display}; /// Trait describing sets of versions. -pub trait VersionSet: Debug + Clone + Eq { +pub trait VersionSet: Debug + Display + Clone + Eq { /// Version type associated with the sets manipulated. - type V; + type V: Clone + Debug + Display; // Constructors /// Constructor for an empty set containing no version. @@ -58,391 +54,3 @@ pub trait VersionSet: Debug + Clone + Eq { .complement() } } - -/// A Range is a set of versions. -#[derive(Debug, Clone, Eq, PartialEq)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct Range { - segments: SmallVec>, -} - -type Interval = (V, Option); - -// Range building blocks. -impl Range { - /// Empty set of versions. - pub fn none() -> Self { - Self { - segments: SmallVec::empty(), - } - } - - /// Set of all possible versions. - pub fn any() -> Self { - Self::higher_than(V::lowest()) - } - - /// Set containing exactly one version. - pub fn exact(v: impl Into) -> Self { - let v = v.into(); - Self { - segments: SmallVec::one((v.clone(), Some(v.bump()))), - } - } - - /// Set of all versions higher or equal to some version. - pub fn higher_than(v: impl Into) -> Self { - Self { - segments: SmallVec::one((v.into(), None)), - } - } - - /// Set of all versions strictly lower than some version. - pub fn strictly_lower_than(v: impl Into) -> Self { - let v = v.into(); - if v == V::lowest() { - Self::none() - } else { - Self { - segments: SmallVec::one((V::lowest(), Some(v))), - } - } - } - - /// Set of all versions comprised between two given versions. - /// The lower bound is included and the higher bound excluded. - /// `v1 <= v < v2`. - pub fn between(v1: impl Into, v2: impl Into) -> Self { - let v1 = v1.into(); - let v2 = v2.into(); - if v1 < v2 { - Self { - segments: SmallVec::one((v1, Some(v2))), - } - } else { - Self::none() - } - } -} - -// Set operations. -impl Range { - // Negate ################################################################## - - /// Compute the complement set of versions. - pub fn negate(&self) -> Self { - match self.segments.first() { - None => Self::any(), // Complement of ∅ is * - - // First high bound is +∞ - Some((v, None)) => { - // Complement of * is ∅ - if v == &V::lowest() { - Self::none() - // Complement of "v <= _" is "_ < v" - } else { - Self::strictly_lower_than(v.clone()) - } - } - - // First high bound is not +∞ - Some((v1, Some(v2))) => { - if v1 == &V::lowest() { - Self::negate_segments(v2.clone(), &self.segments[1..]) - } else { - Self::negate_segments(V::lowest(), &self.segments) - } - } - } - } - - /// Helper function performing the negation of intervals in segments. - /// For example: - /// [ (v1, None) ] => [ (start, Some(v1)) ] - /// [ (v1, Some(v2)) ] => [ (start, Some(v1)), (v2, None) ] - fn negate_segments(start: V, segments: &[Interval]) -> Range { - let mut complement_segments = SmallVec::empty(); - let mut start = Some(start); - for (v1, maybe_v2) in segments { - // start.unwrap() is fine because `segments` is not exposed, - // and our usage guaranties that only the last segment may contain a None. - complement_segments.push((start.unwrap(), Some(v1.to_owned()))); - start = maybe_v2.to_owned(); - } - if let Some(last) = start { - complement_segments.push((last, None)); - } - - Self { - segments: complement_segments, - } - } - - // Union and intersection ################################################## - - /// Compute the union of two sets of versions. - pub fn union(&self, other: &Self) -> Self { - self.negate().intersection(&other.negate()).negate() - } - - /// Compute the intersection of two sets of versions. - pub fn intersection(&self, other: &Self) -> Self { - let mut segments = SmallVec::empty(); - let mut left_iter = self.segments.iter(); - let mut right_iter = other.segments.iter(); - let mut left = left_iter.next(); - let mut right = right_iter.next(); - loop { - match (left, right) { - // Both left and right still contain a finite interval: - (Some((l1, Some(l2))), Some((r1, Some(r2)))) => { - if l2 <= r1 { - // Intervals are disjoint, progress on the left. - left = left_iter.next(); - } else if r2 <= l1 { - // Intervals are disjoint, progress on the right. - right = right_iter.next(); - } else { - // Intervals are not disjoint. - let start = l1.max(r1).to_owned(); - if l2 < r2 { - segments.push((start, Some(l2.to_owned()))); - left = left_iter.next(); - } else { - segments.push((start, Some(r2.to_owned()))); - right = right_iter.next(); - } - } - } - - // Right contains an infinite interval: - (Some((l1, Some(l2))), Some((r1, None))) => match l2.cmp(r1) { - Ordering::Less => { - left = left_iter.next(); - } - Ordering::Equal => { - for l in left_iter.cloned() { - segments.push(l) - } - break; - } - Ordering::Greater => { - let start = l1.max(r1).to_owned(); - segments.push((start, Some(l2.to_owned()))); - for l in left_iter.cloned() { - segments.push(l) - } - break; - } - }, - - // Left contains an infinite interval: - (Some((l1, None)), Some((r1, Some(r2)))) => match r2.cmp(l1) { - Ordering::Less => { - right = right_iter.next(); - } - Ordering::Equal => { - for r in right_iter.cloned() { - segments.push(r) - } - break; - } - Ordering::Greater => { - let start = l1.max(r1).to_owned(); - segments.push((start, Some(r2.to_owned()))); - for r in right_iter.cloned() { - segments.push(r) - } - break; - } - }, - - // Both sides contain an infinite interval: - (Some((l1, None)), Some((r1, None))) => { - let start = l1.max(r1).to_owned(); - segments.push((start, None)); - break; - } - - // Left or right has ended. - _ => { - break; - } - } - } - - Self { segments } - } -} - -// Other useful functions. -impl Range { - /// Check if a range contains a given version. - pub fn contains(&self, version: &V) -> bool { - for (v1, maybe_v2) in &self.segments { - match maybe_v2 { - None => return v1 <= version, - Some(v2) => { - if version < v1 { - return false; - } else if version < v2 { - return true; - } - } - } - } - false - } - - /// Return the lowest version in the range (if there is one). - pub fn lowest_version(&self) -> Option { - self.segments.first().map(|(start, _)| start).cloned() - } -} - -// REPORT ###################################################################### - -impl fmt::Display for Range { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.segments.as_slice() { - [] => write!(f, "∅"), - [(start, None)] if start == &V::lowest() => write!(f, "∗"), - [(start, None)] => write!(f, "{} <= v", start), - [(start, Some(end))] if end == &start.bump() => write!(f, "{}", start), - [(start, Some(end))] if start == &V::lowest() => write!(f, "v < {}", end), - [(start, Some(end))] => write!(f, "{} <= v < {}", start, end), - more_than_one_interval => { - let string_intervals: Vec<_> = more_than_one_interval - .iter() - .map(interval_to_string) - .collect(); - write!(f, "{}", string_intervals.join(" ")) - } - } - } -} - -fn interval_to_string((start, maybe_end): &Interval) -> String { - match maybe_end { - Some(end) => format!("[ {}, {} [", start, end), - None => format!("[ {}, ∞ [", start), - } -} - -// TESTS ####################################################################### - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use crate::version::NumberVersion; - - use super::*; - - pub fn strategy() -> impl Strategy> { - prop::collection::vec(any::(), 0..10).prop_map(|mut vec| { - vec.sort_unstable(); - vec.dedup(); - let mut pair_iter = vec.chunks_exact(2); - let mut segments = SmallVec::empty(); - while let Some([v1, v2]) = pair_iter.next() { - segments.push((NumberVersion(*v1), Some(NumberVersion(*v2)))); - } - if let [v] = pair_iter.remainder() { - segments.push((NumberVersion(*v), None)); - } - Range { segments } - }) - } - - fn version_strat() -> impl Strategy { - any::().prop_map(NumberVersion) - } - - proptest! { - - // Testing negate ---------------------------------- - - #[test] - fn negate_is_different(range in strategy()) { - assert_ne!(range.negate(), range); - } - - #[test] - fn double_negate_is_identity(range in strategy()) { - assert_eq!(range.negate().negate(), range); - } - - #[test] - fn negate_contains_opposite(range in strategy(), version in version_strat()) { - assert_ne!(range.contains(&version), range.negate().contains(&version)); - } - - // Testing intersection ---------------------------- - - #[test] - fn intersection_is_symmetric(r1 in strategy(), r2 in strategy()) { - assert_eq!(r1.intersection(&r2), r2.intersection(&r1)); - } - - #[test] - fn intersection_with_any_is_identity(range in strategy()) { - assert_eq!(Range::any().intersection(&range), range); - } - - #[test] - fn intersection_with_none_is_none(range in strategy()) { - assert_eq!(Range::none().intersection(&range), Range::none()); - } - - #[test] - fn intersection_is_idempotent(r1 in strategy(), r2 in strategy()) { - assert_eq!(r1.intersection(&r2).intersection(&r2), r1.intersection(&r2)); - } - - #[test] - fn intersection_is_associative(r1 in strategy(), r2 in strategy(), r3 in strategy()) { - assert_eq!(r1.intersection(&r2).intersection(&r3), r1.intersection(&r2.intersection(&r3))); - } - - #[test] - fn intesection_of_complements_is_none(range in strategy()) { - assert_eq!(range.negate().intersection(&range), Range::none()); - } - - #[test] - fn intesection_contains_both(r1 in strategy(), r2 in strategy(), version in version_strat()) { - assert_eq!(r1.intersection(&r2).contains(&version), r1.contains(&version) && r2.contains(&version)); - } - - // Testing union ----------------------------------- - - #[test] - fn union_of_complements_is_any(range in strategy()) { - assert_eq!(range.negate().union(&range), Range::any()); - } - - #[test] - fn union_contains_either(r1 in strategy(), r2 in strategy(), version in version_strat()) { - assert_eq!(r1.union(&r2).contains(&version), r1.contains(&version) || r2.contains(&version)); - } - - // Testing contains -------------------------------- - - #[test] - fn always_contains_exact(version in version_strat()) { - assert!(Range::exact(version).contains(&version)); - } - - #[test] - fn contains_negation(range in strategy(), version in version_strat()) { - assert_ne!(range.contains(&version), range.negate().contains(&version)); - } - - #[test] - fn contains_intersection(range in strategy(), version in version_strat()) { - assert_eq!(range.contains(&version), range.intersection(&Range::exact(version)) != Range::none()); - } - } -} From 90e665cfafef746d926c3d64caf7db5f51c0f0f6 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sat, 7 Aug 2021 23:17:47 +0200 Subject: [PATCH 03/14] refactor: port errors to version set --- src/error_bis.rs | 76 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 77 insertions(+) create mode 100644 src/error_bis.rs diff --git a/src/error_bis.rs b/src/error_bis.rs new file mode 100644 index 00000000..c597188f --- /dev/null +++ b/src/error_bis.rs @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! Handling pubgrub errors. + +use thiserror::Error; + +use crate::package::Package; +use crate::report_bis::DerivationTree; +use crate::version_set::VersionSet; + +/// Errors that may occur while solving dependencies. +#[derive(Error, Debug)] +pub enum PubGrubError { + /// There is no solution for this set of dependencies. + #[error("No solution")] + NoSolution(DerivationTree), + + /// Error arising when the implementer of + /// [DependencyProvider](crate::solver::DependencyProvider) + /// returned an error in the method + /// [get_dependencies](crate::solver::DependencyProvider::get_dependencies). + #[error("Retrieving dependencies of {package} {version} failed")] + ErrorRetrievingDependencies { + /// Package whose dependencies we want. + package: P, + /// Version of the package for which we want the dependencies. + version: VS::V, + /// Error raised by the implementer of + /// [DependencyProvider](crate::solver::DependencyProvider). + source: Box, + }, + + /// Error arising when the implementer of + /// [DependencyProvider](crate::solver::DependencyProvider) + /// returned a dependency on an empty range. + /// This technically means that the package can not be selected, + /// but is clearly some kind of mistake. + #[error("Package {dependent} required by {package} {version} depends on the empty set")] + DependencyOnTheEmptySet { + /// Package whose dependencies we want. + package: P, + /// Version of the package for which we want the dependencies. + version: VS::V, + /// The dependent package that requires us to pick from the empty set. + dependent: P, + }, + + /// Error arising when the implementer of + /// [DependencyProvider](crate::solver::DependencyProvider) + /// returned a dependency on the requested package. + /// This technically means that the package directly depends on itself, + /// and is clearly some kind of mistake. + #[error("{package} {version} depends on itself")] + SelfDependency { + /// Package whose dependencies we want. + package: P, + /// Version of the package for which we want the dependencies. + version: VS::V, + }, + + /// Error arising when the implementer of + /// [DependencyProvider](crate::solver::DependencyProvider) + /// returned an error in the method + /// [choose_package_version](crate::solver::DependencyProvider::choose_package_version). + #[error("Decision making failed")] + ErrorChoosingPackageVersion(Box), + + /// Error arising when the implementer of [DependencyProvider](crate::solver::DependencyProvider) + /// returned an error in the method [should_cancel](crate::solver::DependencyProvider::should_cancel). + #[error("We should cancel")] + ErrorInShouldCancel(Box), + + /// Something unexpected happened. + #[error("{0}")] + Failure(String), +} diff --git a/src/lib.rs b/src/lib.rs index 5f96f43a..5ee1e741 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -210,6 +210,7 @@ #![warn(missing_docs)] pub mod error; +pub mod error_bis; pub mod package; pub mod range; pub mod report; From bcd4d2495ad30075dcf5ee9e33b03fe005803375 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sat, 7 Aug 2021 23:38:24 +0200 Subject: [PATCH 04/14] refactor: port partial solution to version set --- src/internal/mod.rs | 1 + src/internal/partial_solution_bis.rs | 452 +++++++++++++++++++++++++++ 2 files changed, 453 insertions(+) create mode 100644 src/internal/partial_solution_bis.rs diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 3f4f3c2c..5c177f2d 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -7,5 +7,6 @@ pub mod core; pub mod incompatibility; pub mod incompatibility_bis; pub mod partial_solution; +pub mod partial_solution_bis; pub mod small_map; pub mod small_vec; diff --git a/src/internal/partial_solution_bis.rs b/src/internal/partial_solution_bis.rs new file mode 100644 index 00000000..20499db1 --- /dev/null +++ b/src/internal/partial_solution_bis.rs @@ -0,0 +1,452 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! A Memory acts like a structured partial solution +//! where terms are regrouped by package in a [Map](crate::type_aliases::Map). + +use crate::internal::arena::Arena; +use crate::internal::incompatibility_bis::{IncompId, Incompatibility, Relation}; +use crate::internal::small_map::SmallMap; +use crate::package::Package; +use crate::term_bis::Term; +use crate::type_aliases::{Map, SelectedDependencies}; +use crate::version_set::VersionSet; + +use super::small_vec::SmallVec; + +#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] +pub struct DecisionLevel(pub u32); + +impl DecisionLevel { + pub fn increment(self) -> Self { + Self(self.0 + 1) + } +} + +/// The partial solution contains all package assignments, +/// organized by package and historically ordered. +#[derive(Clone, Debug)] +pub struct PartialSolution { + next_global_index: u32, + current_decision_level: DecisionLevel, + package_assignments: Map>, +} + +/// Package assignments contain the potential decision and derivations +/// that have already been made for a given package, +/// as well as the intersection of terms by all of these. +#[derive(Clone, Debug)] +struct PackageAssignments { + smallest_decision_level: DecisionLevel, + highest_decision_level: DecisionLevel, + dated_derivations: SmallVec>, + assignments_intersection: AssignmentsIntersection, +} + +#[derive(Clone, Debug)] +pub struct DatedDerivation { + global_index: u32, + decision_level: DecisionLevel, + cause: IncompId, +} + +#[derive(Clone, Debug)] +enum AssignmentsIntersection { + Decision((u32, VS::V, Term)), + Derivations(Term), +} + +#[derive(Clone, Debug)] +pub enum SatisfierSearch { + DifferentDecisionLevels { + previous_satisfier_level: DecisionLevel, + }, + SameDecisionLevels { + satisfier_cause: IncompId, + }, +} + +impl PartialSolution { + /// Initialize an empty PartialSolution. + pub fn empty() -> Self { + Self { + next_global_index: 0, + current_decision_level: DecisionLevel(0), + package_assignments: Map::default(), + } + } + + /// Add a decision. + pub fn add_decision(&mut self, package: P, version: VS::V) { + // Check that add_decision is never used in the wrong context. + if cfg!(debug_assertions) { + match self.package_assignments.get_mut(&package) { + None => panic!("Derivations must already exist"), + Some(pa) => match &pa.assignments_intersection { + // Cannot be called when a decision has already been taken. + AssignmentsIntersection::Decision(_) => panic!("Already existing decision"), + // Cannot be called if the versions is not contained in the terms intersection. + AssignmentsIntersection::Derivations(term) => { + debug_assert!(term.contains(&version)) + } + }, + } + } + self.current_decision_level = self.current_decision_level.increment(); + let mut pa = self + .package_assignments + .get_mut(&package) + .expect("Derivations must already exist"); + pa.highest_decision_level = self.current_decision_level; + pa.assignments_intersection = AssignmentsIntersection::Decision(( + self.next_global_index, + version.clone(), + Term::exact(version), + )); + self.next_global_index += 1; + } + + /// Add a derivation. + pub fn add_derivation( + &mut self, + package: P, + cause: IncompId, + store: &Arena>, + ) { + use std::collections::hash_map::Entry; + let term = store[cause].get(&package).unwrap().negate(); + let dated_derivation = DatedDerivation { + global_index: self.next_global_index, + decision_level: self.current_decision_level, + cause, + }; + self.next_global_index += 1; + match self.package_assignments.entry(package) { + Entry::Occupied(mut occupied) => { + let mut pa = occupied.get_mut(); + pa.highest_decision_level = self.current_decision_level; + match &mut pa.assignments_intersection { + // Check that add_derivation is never called in the wrong context. + AssignmentsIntersection::Decision(_) => { + panic!("add_derivation should not be called after a decision") + } + AssignmentsIntersection::Derivations(t) => { + *t = t.intersection(&term); + } + } + pa.dated_derivations.push(dated_derivation); + } + Entry::Vacant(v) => { + v.insert(PackageAssignments { + smallest_decision_level: self.current_decision_level, + highest_decision_level: self.current_decision_level, + dated_derivations: SmallVec::One([dated_derivation]), + assignments_intersection: AssignmentsIntersection::Derivations(term), + }); + } + } + } + + /// Extract potential packages for the next iteration of unit propagation. + /// Return `None` if there is no suitable package anymore, which stops the algorithm. + /// A package is a potential pick if there isn't an already + /// selected version (no "decision") + /// and if it contains at least one positive derivation term + /// in the partial solution. + pub fn potential_packages(&self) -> Option> { + let mut iter = self + .package_assignments + .iter() + .filter_map(|(p, pa)| pa.assignments_intersection.potential_package_filter(p)) + .peekable(); + if iter.peek().is_some() { + Some(iter) + } else { + None + } + } + + /// If a partial solution has, for every positive derivation, + /// a corresponding decision that satisfies that assignment, + /// it's a total solution and version solving has succeeded. + pub fn extract_solution(&self) -> Option> { + let mut solution = Map::default(); + for (p, pa) in &self.package_assignments { + match &pa.assignments_intersection { + AssignmentsIntersection::Decision((_, v, _)) => { + solution.insert(p.clone(), v.clone()); + } + AssignmentsIntersection::Derivations(term) => { + if term.is_positive() { + return None; + } + } + } + } + Some(solution) + } + + /// Backtrack the partial solution to a given decision level. + pub fn backtrack( + &mut self, + decision_level: DecisionLevel, + store: &Arena>, + ) { + self.current_decision_level = decision_level; + self.package_assignments.retain(|p, pa| { + if pa.smallest_decision_level > decision_level { + // Remove all entries that have a smallest decision level higher than the backtrack target. + false + } else if pa.highest_decision_level <= decision_level { + // Do not change entries older than the backtrack decision level target. + true + } else { + // smallest_decision_level <= decision_level < highest_decision_level + // + // Since decision_level < highest_decision_level, + // We can be certain that there will be no decision in this package assignments + // after backtracking, because such decision would have been the last + // assignment and it would have the "highest_decision_level". + + // Truncate the history. + while pa.dated_derivations.last().map(|dd| dd.decision_level) > Some(decision_level) + { + pa.dated_derivations.pop(); + } + debug_assert!(!pa.dated_derivations.is_empty()); + + // Update highest_decision_level. + pa.highest_decision_level = pa.dated_derivations.last().unwrap().decision_level; + + // Recompute the assignments intersection. + pa.assignments_intersection = AssignmentsIntersection::Derivations( + pa.dated_derivations + .iter() + .fold(Term::any(), |acc, dated_derivation| { + let term = store[dated_derivation.cause].get(&p).unwrap().negate(); + acc.intersection(&term) + }), + ); + 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 fn add_version( + &mut self, + package: P, + version: VS::V, + new_incompatibilities: std::ops::Range>, + store: &Arena>, + ) { + let exact = Term::exact(version.clone()); + let not_satisfied = |incompat: &Incompatibility| { + incompat.relation(|p| { + if p == &package { + Some(&exact) + } else { + self.term_intersection_for_package(p) + } + }) != Relation::Satisfied + }; + + // Check none of the dependencies (new_incompatibilities) + // would create a conflict (be satisfied). + if store[new_incompatibilities].iter().all(not_satisfied) { + self.add_decision(package, version); + } + } + + /// Check if the terms in the partial solution satisfy the incompatibility. + pub fn relation(&self, incompat: &Incompatibility) -> Relation

{ + incompat.relation(|package| self.term_intersection_for_package(package)) + } + + /// Retrieve intersection of terms related to package. + pub fn term_intersection_for_package(&self, package: &P) -> Option<&Term> { + self.package_assignments + .get(package) + .map(|pa| pa.assignments_intersection.term()) + } + + /// Figure out if the satisfier and previous satisfier are of different decision levels. + pub fn satisfier_search( + &self, + incompat: &Incompatibility, + store: &Arena>, + ) -> (P, SatisfierSearch) { + let satisfied_map = Self::find_satisfier(incompat, &self.package_assignments, store); + let (satisfier_package, &(satisfier_index, _, satisfier_decision_level)) = satisfied_map + .iter() + .max_by_key(|(_p, (_, global_index, _))| global_index) + .unwrap(); + let satisfier_package = satisfier_package.clone(); + let previous_satisfier_level = Self::find_previous_satisfier( + incompat, + &satisfier_package, + satisfied_map, + &self.package_assignments, + store, + ); + if previous_satisfier_level < satisfier_decision_level { + let search_result = SatisfierSearch::DifferentDecisionLevels { + previous_satisfier_level, + }; + (satisfier_package, search_result) + } else { + let satisfier_pa = self.package_assignments.get(&satisfier_package).unwrap(); + let dd = &satisfier_pa.dated_derivations[satisfier_index]; + let search_result = SatisfierSearch::SameDecisionLevels { + satisfier_cause: dd.cause, + }; + (satisfier_package, search_result) + } + } + + /// A satisfier is the earliest assignment in partial solution such that the incompatibility + /// is satisfied by the partial solution up to and including that assignment. + /// + /// Returns a map indicating for each package term, when that was first satisfied in history. + /// If we effectively found a satisfier, the returned map must be the same size that incompat. + /// + /// Question: This is possible since we added a "global_index" to every dated_derivation. + /// It would be nice if we could get rid of it, but I don't know if then it will be possible + /// to return a coherent previous_satisfier_level. + fn find_satisfier( + incompat: &Incompatibility, + package_assignments: &Map>, + store: &Arena>, + ) -> SmallMap { + let mut satisfied = SmallMap::Empty; + for (package, incompat_term) in incompat.iter() { + let pa = package_assignments.get(package).expect("Must exist"); + satisfied.insert( + package.clone(), + pa.satisfier(package, incompat_term, Term::any(), store), + ); + } + satisfied + } + + /// Earliest assignment in the partial solution before satisfier + /// such that incompatibility is satisfied by the partial solution up to + /// and including that assignment plus satisfier. + fn find_previous_satisfier( + incompat: &Incompatibility, + satisfier_package: &P, + mut satisfied_map: SmallMap, + package_assignments: &Map>, + store: &Arena>, + ) -> DecisionLevel { + // First, let's retrieve the previous derivations and the initial accum_term. + let satisfier_pa = package_assignments.get(satisfier_package).unwrap(); + let (satisfier_index, _gidx, _dl) = satisfied_map.get_mut(satisfier_package).unwrap(); + + let accum_term = if *satisfier_index == satisfier_pa.dated_derivations.len() { + match &satisfier_pa.assignments_intersection { + AssignmentsIntersection::Derivations(_) => panic!("must be a decision"), + AssignmentsIntersection::Decision((_, _, term)) => term.clone(), + } + } else { + let dd = &satisfier_pa.dated_derivations[*satisfier_index]; + store[dd.cause].get(satisfier_package).unwrap().negate() + }; + + let incompat_term = incompat + .get(satisfier_package) + .expect("satisfier package not in incompat"); + + satisfied_map.insert( + satisfier_package.clone(), + satisfier_pa.satisfier(satisfier_package, incompat_term, accum_term, store), + ); + + // Finally, let's identify the decision level of that previous satisfier. + let (_, &(_, _, decision_level)) = satisfied_map + .iter() + .max_by_key(|(_p, (_, global_index, _))| global_index) + .unwrap(); + decision_level.max(DecisionLevel(1)) + } +} + +impl PackageAssignments { + fn satisfier( + &self, + package: &P, + incompat_term: &Term, + start_term: Term, + store: &Arena>, + ) -> (usize, u32, DecisionLevel) { + // Term where we accumulate intersections until incompat_term is satisfied. + let mut accum_term = start_term; + // Indicate if we found a satisfier in the list of derivations, otherwise it will be the decision. + for (idx, dated_derivation) in self.dated_derivations.iter().enumerate() { + let this_term = store[dated_derivation.cause].get(package).unwrap().negate(); + accum_term = accum_term.intersection(&this_term); + if accum_term.subset_of(incompat_term) { + // We found the derivation causing satisfaction. + return ( + idx, + dated_derivation.global_index, + dated_derivation.decision_level, + ); + } + } + // If it wasn't found in the derivations, + // it must be the decision which is last (if called in the right context). + match self.assignments_intersection { + AssignmentsIntersection::Decision((global_index, _, _)) => ( + self.dated_derivations.len(), + global_index, + self.highest_decision_level, + ), + AssignmentsIntersection::Derivations(_) => { + unreachable!( + concat!( + "while processing package {}: ", + "accum_term = {} isn't a subset of incompat_term = {}, ", + "which means the last assignment should have been a decision, ", + "but instead it was a derivation. This shouldn't be possible! ", + "(Maybe your Version ordering is broken?)" + ), + package, accum_term, incompat_term + ) + } + } + } +} + +impl AssignmentsIntersection { + /// Returns the term intersection of all assignments (decision included). + fn term(&self) -> &Term { + match self { + Self::Decision((_, _, term)) => term, + Self::Derivations(term) => term, + } + } + + /// A package is a potential pick if there isn't an already + /// selected version (no "decision") + /// and if it contains at least one positive derivation term + /// in the partial solution. + fn potential_package_filter<'a, P: Package>( + &'a self, + package: &'a P, + ) -> Option<(&'a P, &'a VS)> { + match self { + Self::Decision(_) => None, + Self::Derivations(term_intersection) => { + if term_intersection.is_positive() { + Some((package, term_intersection.unwrap_positive())) + } else { + None + } + } + } + } +} From 4835f8e6491032e4858d62add1140a15a43e6236 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sat, 7 Aug 2021 23:47:41 +0200 Subject: [PATCH 05/14] refactor: move DependencyConstraints to type_aliases --- src/internal/core.rs | 3 +-- src/solver.rs | 10 +--------- src/type_aliases.rs | 12 +++++++++++- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/internal/core.rs b/src/internal/core.rs index f923850a..55d60984 100644 --- a/src/internal/core.rs +++ b/src/internal/core.rs @@ -15,8 +15,7 @@ use crate::internal::partial_solution::{DecisionLevel, PartialSolution}; use crate::internal::small_vec::SmallVec; use crate::package::Package; use crate::report::DerivationTree; -use crate::solver::DependencyConstraints; -use crate::type_aliases::Map; +use crate::type_aliases::{DependencyConstraints, Map}; use crate::version::Version; /// Current state of the PubGrub algorithm. diff --git a/src/solver.rs b/src/solver.rs index 62015911..2de5f3f3 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -74,7 +74,7 @@ use crate::internal::core::State; use crate::internal::incompatibility::Incompatibility; use crate::package::Package; use crate::range::Range; -use crate::type_aliases::{Map, SelectedDependencies}; +use crate::type_aliases::{DependencyConstraints, Map, SelectedDependencies}; use crate::version::Version; /// Main function of the library. @@ -210,14 +210,6 @@ pub enum Dependencies { Known(DependencyConstraints), } -/// Subtype of [Dependencies] which holds information about -/// all possible versions a given package can accept. -/// There is a difference in semantics between an empty [Map>](crate::type_aliases::Map) -/// inside [DependencyConstraints] and [Dependencies::Unknown]: -/// the former means the package has no dependencies and it is a known fact, -/// while the latter means they could not be fetched by [DependencyProvider]. -pub type DependencyConstraints = Map>; - /// Trait that allows the algorithm to retrieve available packages and their dependencies. /// An implementor needs to be supplied to the [resolve] function. pub trait DependencyProvider { diff --git a/src/type_aliases.rs b/src/type_aliases.rs index d1cc378a..1e5a944c 100644 --- a/src/type_aliases.rs +++ b/src/type_aliases.rs @@ -2,9 +2,19 @@ //! Publicly exported type aliases. +use crate::range::Range; + /// Map implementation used by the library. pub type Map = rustc_hash::FxHashMap; /// Concrete dependencies picked by the library during [resolve](crate::solver::resolve) -/// from [DependencyConstraints](crate::solver::DependencyConstraints) +/// from [DependencyConstraints]. pub type SelectedDependencies = Map; + +/// Subtype of [Dependencies] which holds information about +/// all possible versions a given package can accept. +/// There is a difference in semantics between an empty [Map>](Map) +/// inside [DependencyConstraints] and [Dependencies::Unknown](crate::solver::Dependencies::Unknown): +/// the former means the package has no dependencies and it is a known fact, +/// while the latter means they could not be fetched by [DependencyProvider]. +pub type DependencyConstraints = Map>; From ea44069b75e688fea735f7be1683c7f349946851 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sat, 7 Aug 2021 23:53:47 +0200 Subject: [PATCH 06/14] refactor: port core to version set --- src/internal/core_bis.rs | 269 +++++++++++++++++++++++++++++++++++++++ src/internal/mod.rs | 1 + src/lib.rs | 1 + src/type_aliases_bis.rs | 18 +++ 4 files changed, 289 insertions(+) create mode 100644 src/internal/core_bis.rs create mode 100644 src/type_aliases_bis.rs diff --git a/src/internal/core_bis.rs b/src/internal/core_bis.rs new file mode 100644 index 00000000..594a1a32 --- /dev/null +++ b/src/internal/core_bis.rs @@ -0,0 +1,269 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! Core model and functions +//! to write a functional PubGrub algorithm. + +use std::collections::HashSet as Set; + +use crate::error_bis::PubGrubError; +use crate::internal::arena::Arena; +use crate::internal::incompatibility_bis::{IncompId, Incompatibility, Relation}; +use crate::internal::partial_solution_bis::SatisfierSearch::{ + DifferentDecisionLevels, SameDecisionLevels, +}; +use crate::internal::partial_solution_bis::{DecisionLevel, PartialSolution}; +use crate::internal::small_vec::SmallVec; +use crate::package::Package; +use crate::report_bis::DerivationTree; +use crate::type_aliases_bis::{DependencyConstraints, Map}; +use crate::version_set::VersionSet; + +/// Current state of the PubGrub algorithm. +#[derive(Clone)] +pub struct State { + root_package: P, + root_version: VS::V, + + incompatibilities: Map>>, + + /// Store the ids of incompatibilities that are already contradicted + /// and will stay that way until the next conflict and backtrack is operated. + contradicted_incompatibilities: rustc_hash::FxHashSet>, + + /// Partial solution. + /// TODO: remove pub. + pub partial_solution: PartialSolution, + + /// The store is the reference storage for all incompatibilities. + pub incompatibility_store: Arena>, + + /// This is a stack of work to be done in `unit_propagation`. + /// It can definitely be a local variable to that method, but + /// this way we can reuse the same allocation for better performance. + unit_propagation_buffer: SmallVec

, +} + +impl State { + /// Initialization of PubGrub state. + pub fn init(root_package: P, root_version: VS::V) -> Self { + let mut incompatibility_store = Arena::new(); + let not_root_id = incompatibility_store.alloc(Incompatibility::not_root( + root_package.clone(), + root_version.clone(), + )); + let mut incompatibilities = Map::default(); + incompatibilities.insert(root_package.clone(), vec![not_root_id]); + Self { + root_package, + root_version, + incompatibilities, + contradicted_incompatibilities: rustc_hash::FxHashSet::default(), + partial_solution: PartialSolution::empty(), + incompatibility_store, + unit_propagation_buffer: SmallVec::Empty, + } + } + + /// Add an incompatibility to the state. + pub fn add_incompatibility(&mut self, incompat: Incompatibility) { + let id = self.incompatibility_store.alloc(incompat); + self.merge_incompatibility(id); + } + + /// Add an incompatibility to the state. + pub fn add_incompatibility_from_dependencies( + &mut self, + package: P, + version: VS::V, + deps: &DependencyConstraints, + ) -> std::ops::Range> { + // 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) + })); + // 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); + } + new_incompats_id_range + } + + /// Check if an incompatibility is terminal. + pub fn is_terminal(&self, incompatibility: &Incompatibility) -> bool { + incompatibility.is_terminal(&self.root_package, &self.root_version) + } + + /// Unit propagation is the core mechanism of the solving algorithm. + /// CF + pub fn unit_propagation(&mut self, package: P) -> Result<(), PubGrubError> { + self.unit_propagation_buffer.clear(); + self.unit_propagation_buffer.push(package); + while let Some(current_package) = self.unit_propagation_buffer.pop() { + // Iterate over incompatibilities in reverse order + // to evaluate first the newest incompatibilities. + let mut conflict_id = None; + // We only care about incompatibilities if it contains the current package. + for &incompat_id in self.incompatibilities[¤t_package].iter().rev() { + if self.contradicted_incompatibilities.contains(&incompat_id) { + continue; + } + let current_incompat = &self.incompatibility_store[incompat_id]; + match self.partial_solution.relation(current_incompat) { + // If the partial solution satisfies the incompatibility + // we must perform conflict resolution. + Relation::Satisfied => { + conflict_id = Some(incompat_id); + break; + } + Relation::AlmostSatisfied(package_almost) => { + self.unit_propagation_buffer.push(package_almost.clone()); + // Add (not term) to the partial solution with incompat as cause. + self.partial_solution.add_derivation( + package_almost, + incompat_id, + &self.incompatibility_store, + ); + // With the partial solution updated, the incompatibility is now contradicted. + self.contradicted_incompatibilities.insert(incompat_id); + } + Relation::Contradicted(_) => { + self.contradicted_incompatibilities.insert(incompat_id); + } + _ => {} + } + } + if let Some(incompat_id) = conflict_id { + let (package_almost, root_cause) = self.conflict_resolution(incompat_id)?; + self.unit_propagation_buffer.clear(); + self.unit_propagation_buffer.push(package_almost.clone()); + // Add to the partial solution with incompat as cause. + self.partial_solution.add_derivation( + package_almost, + root_cause, + &self.incompatibility_store, + ); + // After conflict resolution and the partial solution update, + // the root cause incompatibility is now contradicted. + self.contradicted_incompatibilities.insert(root_cause); + } + } + // If there are no more changed packages, unit propagation is done. + Ok(()) + } + + /// Return the root cause and the backtracked model. + /// CF + fn conflict_resolution( + &mut self, + incompatibility: IncompId, + ) -> Result<(P, IncompId), PubGrubError> { + let mut current_incompat_id = incompatibility; + let mut current_incompat_changed = false; + loop { + if self.incompatibility_store[current_incompat_id] + .is_terminal(&self.root_package, &self.root_version) + { + return Err(PubGrubError::NoSolution( + self.build_derivation_tree(current_incompat_id), + )); + } else { + let (package, satisfier_search_result) = self.partial_solution.satisfier_search( + &self.incompatibility_store[current_incompat_id], + &self.incompatibility_store, + ); + match satisfier_search_result { + DifferentDecisionLevels { + previous_satisfier_level, + } => { + self.backtrack( + current_incompat_id, + current_incompat_changed, + previous_satisfier_level, + ); + return Ok((package, current_incompat_id)); + } + SameDecisionLevels { satisfier_cause } => { + let prior_cause = Incompatibility::prior_cause( + current_incompat_id, + satisfier_cause, + &package, + &self.incompatibility_store, + ); + current_incompat_id = self.incompatibility_store.alloc(prior_cause); + current_incompat_changed = true; + } + } + } + } + } + + /// Backtracking. + fn backtrack( + &mut self, + incompat: IncompId, + incompat_changed: bool, + decision_level: DecisionLevel, + ) { + self.partial_solution + .backtrack(decision_level, &self.incompatibility_store); + self.contradicted_incompatibilities.clear(); + if incompat_changed { + self.merge_incompatibility(incompat); + } + } + + /// Add this incompatibility into the set of all incompatibilities. + /// + /// Pub collapses identical dependencies from adjacent package versions + /// into individual incompatibilities. + /// This substantially reduces the total number of incompatibilities + /// and makes it much easier for Pub to reason about multiple versions of packages at once. + /// + /// For example, rather than representing + /// foo 1.0.0 depends on bar ^1.0.0 and + /// foo 1.1.0 depends on bar ^1.0.0 + /// as two separate incompatibilities, + /// they are collapsed together into the single incompatibility {foo ^1.0.0, not bar ^1.0.0} + /// (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) { + for (pkg, _term) in self.incompatibility_store[id].iter() { + self.incompatibilities + .entry(pkg.clone()) + .or_default() + .push(id); + } + } + + // Error reporting ######################################################### + + fn build_derivation_tree(&self, incompat: IncompId) -> DerivationTree { + let shared_ids = self.find_shared_ids(incompat); + Incompatibility::build_derivation_tree(incompat, &shared_ids, &self.incompatibility_store) + } + + fn find_shared_ids(&self, incompat: IncompId) -> Set> { + let mut all_ids = Set::new(); + let mut shared_ids = Set::new(); + let mut stack = vec![incompat]; + while let Some(i) = stack.pop() { + if let Some((id1, id2)) = self.incompatibility_store[i].causes() { + if all_ids.contains(&i) { + shared_ids.insert(i); + } else { + all_ids.insert(i); + stack.push(id1); + stack.push(id2); + } + } + } + shared_ids + } +} diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 5c177f2d..808819ea 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -4,6 +4,7 @@ pub mod arena; pub mod core; +pub mod core_bis; pub mod incompatibility; pub mod incompatibility_bis; pub mod partial_solution; diff --git a/src/lib.rs b/src/lib.rs index 5ee1e741..8dab16b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -219,6 +219,7 @@ pub mod solver; pub mod term; pub mod term_bis; pub mod type_aliases; +pub mod type_aliases_bis; pub mod version; pub mod version_set; diff --git a/src/type_aliases_bis.rs b/src/type_aliases_bis.rs new file mode 100644 index 00000000..11ecf38d --- /dev/null +++ b/src/type_aliases_bis.rs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! Publicly exported type aliases. + +/// Map implementation used by the library. +pub type Map = rustc_hash::FxHashMap; + +/// Concrete dependencies picked by the library during [resolve](crate::solver::resolve) +/// from [DependencyConstraints]. +pub type SelectedDependencies = Map; + +/// Subtype of [Dependencies] which holds information about +/// all possible versions a given package can accept. +/// There is a difference in semantics between an empty [Map>](Map) +/// inside [DependencyConstraints] and [Dependencies::Unknown](crate::solver::Dependencies::Unknown): +/// the former means the package has no dependencies and it is a known fact, +/// while the latter means they could not be fetched by [DependencyProvider]. +pub type DependencyConstraints = Map; From b7568d4c33ae9e8935f845d7c4fd8942a3698e78 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sun, 8 Aug 2021 00:11:26 +0200 Subject: [PATCH 07/14] refactor: port solver to version set --- src/lib.rs | 1 + src/solver_bis.rs | 384 +++++++++++++++++++++++++++++++++++++++++++++ src/version_set.rs | 2 +- 3 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 src/solver_bis.rs diff --git a/src/lib.rs b/src/lib.rs index 8dab16b7..85670342 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -216,6 +216,7 @@ pub mod range; pub mod report; pub mod report_bis; pub mod solver; +pub mod solver_bis; pub mod term; pub mod term_bis; pub mod type_aliases; diff --git a/src/solver_bis.rs b/src/solver_bis.rs new file mode 100644 index 00000000..63fc6848 --- /dev/null +++ b/src/solver_bis.rs @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: MPL-2.0 + +//! PubGrub version solving algorithm. +//! +//! It consists in efficiently finding a set of packages and versions +//! that satisfy all the constraints of a given project dependencies. +//! In addition, when that is not possible, +//! PubGrub tries to provide a very human-readable and clear +//! explanation as to why that failed. +//! Below is an example of explanation present in +//! the introductory blog post about PubGrub +//! +//! ```txt +//! Because dropdown >=2.0.0 depends on icons >=2.0.0 and +//! root depends on icons <2.0.0, dropdown >=2.0.0 is forbidden. +//! +//! And because menu >=1.1.0 depends on dropdown >=2.0.0, +//! menu >=1.1.0 is forbidden. +//! +//! And because menu <1.1.0 depends on dropdown >=1.0.0 <2.0.0 +//! which depends on intl <4.0.0, every version of menu +//! requires intl <4.0.0. +//! +//! So, because root depends on both menu >=1.0.0 and intl >=5.0.0, +//! version solving failed. +//! ``` +//! +//! The algorithm is generic and works for any type of dependency system +//! as long as packages (P) and versions (V) implement +//! the [Package](crate::package::Package) and [Version](crate::version::Version) traits. +//! [Package](crate::package::Package) is strictly equivalent and automatically generated +//! for any type that implement [Clone] + [Eq] + [Hash] + [Debug] + [Display](std::fmt::Display). +//! [Version](crate::version::Version) simply states that versions are ordered, +//! that there should be +//! a minimal [lowest](crate::version::Version::lowest) version (like 0.0.0 in semantic versions), +//! and that for any version, it is possible to compute +//! what the next version closest to this one is ([bump](crate::version::Version::bump)). +//! For semantic versions, [bump](crate::version::Version::bump) corresponds to +//! an increment of the patch number. +//! +//! ## API +//! +//! ``` +//! # use pubgrub::solver::{resolve, OfflineDependencyProvider}; +//! # use pubgrub::version::NumberVersion; +//! # use pubgrub::error::PubGrubError; +//! # +//! # fn try_main() -> Result<(), PubGrubError<&'static str, NumberVersion>> { +//! # let dependency_provider = OfflineDependencyProvider::<&str, NumberVersion>::new(); +//! # let package = "root"; +//! # let version = 1; +//! let solution = resolve(&dependency_provider, package, version)?; +//! # Ok(()) +//! # } +//! # fn main() { +//! # assert!(matches!(try_main(), Err(PubGrubError::NoSolution(_)))); +//! # } +//! ``` +//! +//! Where `dependency_provider` supplies the list of available packages and versions, +//! as well as the dependencies of every available package +//! by implementing the [DependencyProvider] trait. +//! The call to [resolve] for a given package at a given version +//! will compute the set of packages and versions needed +//! to satisfy the dependencies of that package and version pair. +//! If there is no solution, the reason will be provided as clear as possible. + +use std::borrow::Borrow; +use std::collections::{BTreeMap, BTreeSet as Set}; +use std::error::Error; + +use crate::error_bis::PubGrubError; +use crate::internal::core_bis::State; +use crate::internal::incompatibility_bis::Incompatibility; +use crate::package::Package; +use crate::type_aliases_bis::{DependencyConstraints, Map, SelectedDependencies}; +use crate::version_set::VersionSet; + +/// Main function of the library. +/// Finds a set of packages satisfying dependency bounds for a given package + version pair. +pub fn resolve( + dependency_provider: &impl DependencyProvider, + package: P, + version: impl Into, +) -> Result, PubGrubError> { + let mut state = State::init(package.clone(), version.into()); + let mut added_dependencies: Map> = Map::default(); + let mut next = package; + loop { + dependency_provider + .should_cancel() + .map_err(|err| PubGrubError::ErrorInShouldCancel(err))?; + + state.unit_propagation(next)?; + + let potential_packages = state.partial_solution.potential_packages(); + if potential_packages.is_none() { + drop(potential_packages); + // The borrow checker did not like using a match on potential_packages. + // This `if ... is_none ... drop` is a workaround. + // I believe this is a case where Polonius could help, when and if it lands in rustc. + return state.partial_solution.extract_solution().ok_or_else(|| { + PubGrubError::Failure( + "How did we end up with no package to choose but no solution?".into(), + ) + }); + } + let decision = dependency_provider + .choose_package_version(potential_packages.unwrap()) + .map_err(PubGrubError::ErrorChoosingPackageVersion)?; + next = decision.0.clone(); + + // Pick the next compatible version. + let term_intersection = state + .partial_solution + .term_intersection_for_package(&next) + .expect("a package was chosen but we don't have a term."); + let v = match decision.1 { + None => { + let inc = Incompatibility::no_versions(next.clone(), term_intersection.clone()); + state.add_incompatibility(inc); + continue; + } + Some(x) => x, + }; + if !term_intersection.contains(&v) { + return Err(PubGrubError::ErrorChoosingPackageVersion( + "choose_package_version picked an incompatible version".into(), + )); + } + + if added_dependencies + .entry(next.clone()) + .or_default() + .insert(v.clone()) + { + // Retrieve that package dependencies. + let p = &next; + let dependencies = + match dependency_provider + .get_dependencies(&p, &v) + .map_err(|err| PubGrubError::ErrorRetrievingDependencies { + package: p.clone(), + version: v.clone(), + source: err, + })? { + Dependencies::Unknown => { + state.add_incompatibility(Incompatibility::unavailable_dependencies( + p.clone(), + v.clone(), + )); + continue; + } + Dependencies::Known(x) => { + if x.contains_key(&p) { + return Err(PubGrubError::SelfDependency { + package: p.clone(), + version: v.clone(), + }); + } + if let Some((dependent, _)) = x.iter().find(|(_, r)| r == &&VS::empty()) { + return Err(PubGrubError::DependencyOnTheEmptySet { + package: p.clone(), + version: v.clone(), + dependent: dependent.clone(), + }); + } + x + } + }; + + // Add that package and version if the dependencies are not problematic. + let dep_incompats = + state.add_incompatibility_from_dependencies(p.clone(), v.clone(), &dependencies); + + // TODO: I don't think this check can actually happen. + // We might want to put it under #[cfg(debug_assertions)]. + if state.incompatibility_store[dep_incompats.clone()] + .iter() + .any(|incompat| state.is_terminal(incompat)) + { + // For a dependency incompatibility to be terminal, + // it can only mean that root depend on not root? + return Err(PubGrubError::Failure( + "Root package depends on itself at a different version?".into(), + )); + } + state.partial_solution.add_version( + p.clone(), + v, + dep_incompats, + &state.incompatibility_store, + ); + } else { + // `dep_incompats` are already in `incompatibilities` so we know there are not satisfied + // terms and can add the decision directly. + state.partial_solution.add_decision(next.clone(), v); + } + } +} + +/// An enum used by [DependencyProvider] that holds information about package dependencies. +/// For each [Package] there is a [Range] of concrete versions it allows as a dependency. +#[derive(Clone)] +pub enum Dependencies { + /// Package dependencies are unavailable. + Unknown, + /// Container for all available package versions. + Known(DependencyConstraints), +} + +/// Trait that allows the algorithm to retrieve available packages and their dependencies. +/// An implementor needs to be supplied to the [resolve] function. +pub trait DependencyProvider { + /// [Decision making](https://github.com/dart-lang/pub/blob/master/doc/solver.md#decision-making) + /// is the process of choosing the next package + /// and version that will be appended to the partial solution. + /// Every time such a decision must be made, + /// potential valid packages and version ranges are preselected by the resolver, + /// and the dependency provider must choose. + /// + /// The strategy employed to choose such package and version + /// cannot change the existence of a solution or not, + /// but can drastically change the performances of the solver, + /// or the properties of the solution. + /// The documentation of Pub (PubGrub implementation for the dart programming language) + /// states the following: + /// + /// > Pub chooses the latest matching version of the package + /// > with the fewest versions that match the outstanding constraint. + /// > This tends to find conflicts earlier if any exist, + /// > since these packages will run out of versions to try more quickly. + /// > But there's likely room for improvement in these heuristics. + /// + /// A helper function [choose_package_with_fewest_versions] is provided to ease + /// implementations of this method if you can produce an iterator + /// of the available versions in preference order for any package. + /// + /// Note: the type `T` ensures that this returns an item from the `packages` argument. + fn choose_package_version, U: Borrow>( + &self, + potential_packages: impl Iterator, + ) -> Result<(T, Option), Box>; + + /// Retrieves the package dependencies. + /// Return [Dependencies::Unknown] if its dependencies are unknown. + fn get_dependencies( + &self, + package: &P, + version: &VS::V, + ) -> Result, Box>; + + /// This is called fairly regularly during the resolution, + /// if it returns an Err then resolution will be terminated. + /// This is helpful if you want to add some form of early termination like a timeout, + /// or you want to add some form of user feedback if things are taking a while. + /// If not provided the resolver will run as long as needed. + fn should_cancel(&self) -> Result<(), Box> { + Ok(()) + } +} + +/// This is a helper function to make it easy to implement +/// [DependencyProvider::choose_package_version]. +/// It takes a function `list_available_versions` that takes a package and returns an iterator +/// of the available versions in preference order. +/// The helper finds the package from the `packages` argument with the fewest versions from +/// `list_available_versions` contained in the constraints. Then takes that package and finds the +/// first version contained in the constraints. +pub fn choose_package_with_fewest_versions( + list_available_versions: F, + potential_packages: impl Iterator, +) -> (T, Option) +where + T: Borrow

, + U: Borrow, + I: Iterator, + F: Fn(&P) -> I, +{ + let count_valid = |(p, set): &(T, U)| { + list_available_versions(p.borrow()) + .filter(|v| set.borrow().contains(v.borrow())) + .count() + }; + let (pkg, set) = potential_packages + .min_by_key(count_valid) + .expect("potential_packages gave us an empty iterator"); + let version = list_available_versions(pkg.borrow()).find(|v| set.borrow().contains(v.borrow())); + (pkg, version) +} + +/// A basic implementation of [DependencyProvider]. +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct OfflineDependencyProvider { + dependencies: Map>>, +} + +impl OfflineDependencyProvider { + /// Creates an empty OfflineDependencyProvider with no dependencies. + pub fn new() -> Self { + Self { + dependencies: Map::default(), + } + } + + /// Registers the dependencies of a package and version pair. + /// Dependencies must be added with a single call to + /// [add_dependencies](OfflineDependencyProvider::add_dependencies). + /// All subsequent calls to + /// [add_dependencies](OfflineDependencyProvider::add_dependencies) for a given + /// package version pair will replace the dependencies by the new ones. + /// + /// The API does not allow to add dependencies one at a time to uphold an assumption that + /// [OfflineDependencyProvider.get_dependencies(p, v)](OfflineDependencyProvider::get_dependencies) + /// provides all dependencies of a given package (p) and version (v) pair. + pub fn add_dependencies>( + &mut self, + package: P, + version: impl Into, + dependencies: I, + ) { + let package_deps = dependencies.into_iter().collect(); + let v = version.into(); + *self + .dependencies + .entry(package) + .or_default() + .entry(v) + .or_default() = package_deps; + } + + /// Lists packages that have been saved. + pub fn packages(&self) -> impl Iterator { + self.dependencies.keys() + } + + /// Lists versions of saved packages in sorted order. + /// Returns [None] if no information is available regarding that package. + pub fn versions(&self, package: &P) -> Option> { + self.dependencies.get(package).map(|k| k.keys()) + } + + /// Lists dependencies of a given package and version. + /// Returns [None] if no information is available regarding that package and version pair. + fn dependencies(&self, package: &P, version: &VS::V) -> Option> { + self.dependencies.get(package)?.get(version).cloned() + } +} + +/// An implementation of [DependencyProvider] that +/// contains all dependency information available in memory. +/// Packages are picked with the fewest versions contained in the constraints first. +/// Versions are picked with the newest versions first. +impl DependencyProvider for OfflineDependencyProvider { + fn choose_package_version, U: Borrow>( + &self, + potential_packages: impl Iterator, + ) -> Result<(T, Option), Box> { + Ok(choose_package_with_fewest_versions( + |p| { + self.dependencies + .get(p) + .into_iter() + .flat_map(|k| k.keys()) + .rev() + .cloned() + }, + potential_packages, + )) + } + + fn get_dependencies( + &self, + package: &P, + version: &VS::V, + ) -> Result, Box> { + Ok(match self.dependencies(package, version) { + None => Dependencies::Unknown, + Some(dependencies) => Dependencies::Known(dependencies), + }) + } +} diff --git a/src/version_set.rs b/src/version_set.rs index 65454e04..8cd1be47 100644 --- a/src/version_set.rs +++ b/src/version_set.rs @@ -19,7 +19,7 @@ use std::fmt::{Debug, Display}; /// Trait describing sets of versions. pub trait VersionSet: Debug + Display + Clone + Eq { /// Version type associated with the sets manipulated. - type V: Clone + Debug + Display; + type V: Debug + Display + Clone + Ord; // Constructors /// Constructor for an empty set containing no version. From 26385d04fd9526563fd950b5869cfff2827cbfc5 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sun, 8 Aug 2021 00:20:29 +0200 Subject: [PATCH 08/14] refactor: replace old modules with ones based on version_set --- src/error.rs | 12 +- src/error_bis.rs | 76 ----- src/internal/core.rs | 42 +-- src/internal/core_bis.rs | 269 --------------- src/internal/incompatibility.rs | 86 ++--- src/internal/incompatibility_bis.rs | 296 ----------------- src/internal/mod.rs | 3 - src/internal/partial_solution.rs | 85 +++-- src/internal/partial_solution_bis.rs | 452 ------------------------- src/lib.rs | 5 - src/report.rs | 152 +++++---- src/report_bis.rs | 481 --------------------------- src/solver.rs | 70 ++-- src/solver_bis.rs | 384 --------------------- src/term.rs | 77 ++--- src/term_bis.rs | 213 ------------ src/type_aliases.rs | 4 +- src/type_aliases_bis.rs | 18 - 18 files changed, 260 insertions(+), 2465 deletions(-) delete mode 100644 src/error_bis.rs delete mode 100644 src/internal/core_bis.rs delete mode 100644 src/internal/incompatibility_bis.rs delete mode 100644 src/internal/partial_solution_bis.rs delete mode 100644 src/report_bis.rs delete mode 100644 src/solver_bis.rs delete mode 100644 src/term_bis.rs delete mode 100644 src/type_aliases_bis.rs diff --git a/src/error.rs b/src/error.rs index 0553d8de..1098706c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -6,14 +6,14 @@ use thiserror::Error; use crate::package::Package; use crate::report::DerivationTree; -use crate::version::Version; +use crate::version_set::VersionSet; /// Errors that may occur while solving dependencies. #[derive(Error, Debug)] -pub enum PubGrubError { +pub enum PubGrubError { /// There is no solution for this set of dependencies. #[error("No solution")] - NoSolution(DerivationTree), + NoSolution(DerivationTree), /// Error arising when the implementer of /// [DependencyProvider](crate::solver::DependencyProvider) @@ -24,7 +24,7 @@ pub enum PubGrubError { /// Package whose dependencies we want. package: P, /// Version of the package for which we want the dependencies. - version: V, + version: VS::V, /// Error raised by the implementer of /// [DependencyProvider](crate::solver::DependencyProvider). source: Box, @@ -40,7 +40,7 @@ pub enum PubGrubError { /// Package whose dependencies we want. package: P, /// Version of the package for which we want the dependencies. - version: V, + version: VS::V, /// The dependent package that requires us to pick from the empty set. dependent: P, }, @@ -55,7 +55,7 @@ pub enum PubGrubError { /// Package whose dependencies we want. package: P, /// Version of the package for which we want the dependencies. - version: V, + version: VS::V, }, /// Error arising when the implementer of diff --git a/src/error_bis.rs b/src/error_bis.rs deleted file mode 100644 index c597188f..00000000 --- a/src/error_bis.rs +++ /dev/null @@ -1,76 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! Handling pubgrub errors. - -use thiserror::Error; - -use crate::package::Package; -use crate::report_bis::DerivationTree; -use crate::version_set::VersionSet; - -/// Errors that may occur while solving dependencies. -#[derive(Error, Debug)] -pub enum PubGrubError { - /// There is no solution for this set of dependencies. - #[error("No solution")] - NoSolution(DerivationTree), - - /// Error arising when the implementer of - /// [DependencyProvider](crate::solver::DependencyProvider) - /// returned an error in the method - /// [get_dependencies](crate::solver::DependencyProvider::get_dependencies). - #[error("Retrieving dependencies of {package} {version} failed")] - ErrorRetrievingDependencies { - /// Package whose dependencies we want. - package: P, - /// Version of the package for which we want the dependencies. - version: VS::V, - /// Error raised by the implementer of - /// [DependencyProvider](crate::solver::DependencyProvider). - source: Box, - }, - - /// Error arising when the implementer of - /// [DependencyProvider](crate::solver::DependencyProvider) - /// returned a dependency on an empty range. - /// This technically means that the package can not be selected, - /// but is clearly some kind of mistake. - #[error("Package {dependent} required by {package} {version} depends on the empty set")] - DependencyOnTheEmptySet { - /// Package whose dependencies we want. - package: P, - /// Version of the package for which we want the dependencies. - version: VS::V, - /// The dependent package that requires us to pick from the empty set. - dependent: P, - }, - - /// Error arising when the implementer of - /// [DependencyProvider](crate::solver::DependencyProvider) - /// returned a dependency on the requested package. - /// This technically means that the package directly depends on itself, - /// and is clearly some kind of mistake. - #[error("{package} {version} depends on itself")] - SelfDependency { - /// Package whose dependencies we want. - package: P, - /// Version of the package for which we want the dependencies. - version: VS::V, - }, - - /// Error arising when the implementer of - /// [DependencyProvider](crate::solver::DependencyProvider) - /// returned an error in the method - /// [choose_package_version](crate::solver::DependencyProvider::choose_package_version). - #[error("Decision making failed")] - ErrorChoosingPackageVersion(Box), - - /// Error arising when the implementer of [DependencyProvider](crate::solver::DependencyProvider) - /// returned an error in the method [should_cancel](crate::solver::DependencyProvider::should_cancel). - #[error("We should cancel")] - ErrorInShouldCancel(Box), - - /// Something unexpected happened. - #[error("{0}")] - Failure(String), -} diff --git a/src/internal/core.rs b/src/internal/core.rs index 55d60984..87967129 100644 --- a/src/internal/core.rs +++ b/src/internal/core.rs @@ -16,26 +16,26 @@ use crate::internal::small_vec::SmallVec; use crate::package::Package; use crate::report::DerivationTree; use crate::type_aliases::{DependencyConstraints, Map}; -use crate::version::Version; +use crate::version_set::VersionSet; /// Current state of the PubGrub algorithm. #[derive(Clone)] -pub struct State { +pub struct State { root_package: P, - root_version: V, + root_version: VS::V, - incompatibilities: Map>>, + incompatibilities: Map>>, /// Store the ids of incompatibilities that are already contradicted /// and will stay that way until the next conflict and backtrack is operated. - contradicted_incompatibilities: rustc_hash::FxHashSet>, + contradicted_incompatibilities: rustc_hash::FxHashSet>, /// Partial solution. /// TODO: remove pub. - pub partial_solution: PartialSolution, + pub partial_solution: PartialSolution, /// The store is the reference storage for all incompatibilities. - pub incompatibility_store: Arena>, + pub incompatibility_store: Arena>, /// This is a stack of work to be done in `unit_propagation`. /// It can definitely be a local variable to that method, but @@ -43,9 +43,9 @@ pub struct State { unit_propagation_buffer: SmallVec

, } -impl State { +impl State { /// Initialization of PubGrub state. - pub fn init(root_package: P, root_version: V) -> Self { + pub fn init(root_package: P, root_version: VS::V) -> Self { let mut incompatibility_store = Arena::new(); let not_root_id = incompatibility_store.alloc(Incompatibility::not_root( root_package.clone(), @@ -65,7 +65,7 @@ impl State { } /// Add an incompatibility to the state. - pub fn add_incompatibility(&mut self, incompat: Incompatibility) { + pub fn add_incompatibility(&mut self, incompat: Incompatibility) { let id = self.incompatibility_store.alloc(incompat); self.merge_incompatibility(id); } @@ -74,9 +74,9 @@ impl State { pub fn add_incompatibility_from_dependencies( &mut self, package: P, - version: V, - deps: &DependencyConstraints, - ) -> std::ops::Range> { + version: VS::V, + deps: &DependencyConstraints, + ) -> std::ops::Range> { // Create incompatibilities and allocate them in the store. let new_incompats_id_range = self .incompatibility_store @@ -91,13 +91,13 @@ impl State { } /// Check if an incompatibility is terminal. - pub fn is_terminal(&self, incompatibility: &Incompatibility) -> bool { + pub fn is_terminal(&self, incompatibility: &Incompatibility) -> bool { incompatibility.is_terminal(&self.root_package, &self.root_version) } /// Unit propagation is the core mechanism of the solving algorithm. /// CF - pub fn unit_propagation(&mut self, package: P) -> Result<(), PubGrubError> { + pub fn unit_propagation(&mut self, package: P) -> Result<(), PubGrubError> { self.unit_propagation_buffer.clear(); self.unit_propagation_buffer.push(package); while let Some(current_package) = self.unit_propagation_buffer.pop() { @@ -157,8 +157,8 @@ impl State { /// CF fn conflict_resolution( &mut self, - incompatibility: IncompId, - ) -> Result<(P, IncompId), PubGrubError> { + incompatibility: IncompId, + ) -> Result<(P, IncompId), PubGrubError> { let mut current_incompat_id = incompatibility; let mut current_incompat_changed = false; loop { @@ -202,7 +202,7 @@ impl State { /// Backtracking. fn backtrack( &mut self, - incompat: IncompId, + incompat: IncompId, incompat_changed: bool, decision_level: DecisionLevel, ) { @@ -233,7 +233,7 @@ impl State { /// 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) { + fn merge_incompatibility(&mut self, id: IncompId) { for (pkg, _term) in self.incompatibility_store[id].iter() { self.incompatibilities .entry(pkg.clone()) @@ -244,12 +244,12 @@ impl State { // Error reporting ######################################################### - fn build_derivation_tree(&self, incompat: IncompId) -> DerivationTree { + fn build_derivation_tree(&self, incompat: IncompId) -> DerivationTree { let shared_ids = self.find_shared_ids(incompat); Incompatibility::build_derivation_tree(incompat, &shared_ids, &self.incompatibility_store) } - fn find_shared_ids(&self, incompat: IncompId) -> Set> { + fn find_shared_ids(&self, incompat: IncompId) -> Set> { let mut all_ids = Set::new(); let mut shared_ids = Set::new(); let mut stack = vec![incompat]; diff --git a/src/internal/core_bis.rs b/src/internal/core_bis.rs deleted file mode 100644 index 594a1a32..00000000 --- a/src/internal/core_bis.rs +++ /dev/null @@ -1,269 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! Core model and functions -//! to write a functional PubGrub algorithm. - -use std::collections::HashSet as Set; - -use crate::error_bis::PubGrubError; -use crate::internal::arena::Arena; -use crate::internal::incompatibility_bis::{IncompId, Incompatibility, Relation}; -use crate::internal::partial_solution_bis::SatisfierSearch::{ - DifferentDecisionLevels, SameDecisionLevels, -}; -use crate::internal::partial_solution_bis::{DecisionLevel, PartialSolution}; -use crate::internal::small_vec::SmallVec; -use crate::package::Package; -use crate::report_bis::DerivationTree; -use crate::type_aliases_bis::{DependencyConstraints, Map}; -use crate::version_set::VersionSet; - -/// Current state of the PubGrub algorithm. -#[derive(Clone)] -pub struct State { - root_package: P, - root_version: VS::V, - - incompatibilities: Map>>, - - /// Store the ids of incompatibilities that are already contradicted - /// and will stay that way until the next conflict and backtrack is operated. - contradicted_incompatibilities: rustc_hash::FxHashSet>, - - /// Partial solution. - /// TODO: remove pub. - pub partial_solution: PartialSolution, - - /// The store is the reference storage for all incompatibilities. - pub incompatibility_store: Arena>, - - /// This is a stack of work to be done in `unit_propagation`. - /// It can definitely be a local variable to that method, but - /// this way we can reuse the same allocation for better performance. - unit_propagation_buffer: SmallVec

, -} - -impl State { - /// Initialization of PubGrub state. - pub fn init(root_package: P, root_version: VS::V) -> Self { - let mut incompatibility_store = Arena::new(); - let not_root_id = incompatibility_store.alloc(Incompatibility::not_root( - root_package.clone(), - root_version.clone(), - )); - let mut incompatibilities = Map::default(); - incompatibilities.insert(root_package.clone(), vec![not_root_id]); - Self { - root_package, - root_version, - incompatibilities, - contradicted_incompatibilities: rustc_hash::FxHashSet::default(), - partial_solution: PartialSolution::empty(), - incompatibility_store, - unit_propagation_buffer: SmallVec::Empty, - } - } - - /// Add an incompatibility to the state. - pub fn add_incompatibility(&mut self, incompat: Incompatibility) { - let id = self.incompatibility_store.alloc(incompat); - self.merge_incompatibility(id); - } - - /// Add an incompatibility to the state. - pub fn add_incompatibility_from_dependencies( - &mut self, - package: P, - version: VS::V, - deps: &DependencyConstraints, - ) -> std::ops::Range> { - // 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) - })); - // 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); - } - new_incompats_id_range - } - - /// Check if an incompatibility is terminal. - pub fn is_terminal(&self, incompatibility: &Incompatibility) -> bool { - incompatibility.is_terminal(&self.root_package, &self.root_version) - } - - /// Unit propagation is the core mechanism of the solving algorithm. - /// CF - pub fn unit_propagation(&mut self, package: P) -> Result<(), PubGrubError> { - self.unit_propagation_buffer.clear(); - self.unit_propagation_buffer.push(package); - while let Some(current_package) = self.unit_propagation_buffer.pop() { - // Iterate over incompatibilities in reverse order - // to evaluate first the newest incompatibilities. - let mut conflict_id = None; - // We only care about incompatibilities if it contains the current package. - for &incompat_id in self.incompatibilities[¤t_package].iter().rev() { - if self.contradicted_incompatibilities.contains(&incompat_id) { - continue; - } - let current_incompat = &self.incompatibility_store[incompat_id]; - match self.partial_solution.relation(current_incompat) { - // If the partial solution satisfies the incompatibility - // we must perform conflict resolution. - Relation::Satisfied => { - conflict_id = Some(incompat_id); - break; - } - Relation::AlmostSatisfied(package_almost) => { - self.unit_propagation_buffer.push(package_almost.clone()); - // Add (not term) to the partial solution with incompat as cause. - self.partial_solution.add_derivation( - package_almost, - incompat_id, - &self.incompatibility_store, - ); - // With the partial solution updated, the incompatibility is now contradicted. - self.contradicted_incompatibilities.insert(incompat_id); - } - Relation::Contradicted(_) => { - self.contradicted_incompatibilities.insert(incompat_id); - } - _ => {} - } - } - if let Some(incompat_id) = conflict_id { - let (package_almost, root_cause) = self.conflict_resolution(incompat_id)?; - self.unit_propagation_buffer.clear(); - self.unit_propagation_buffer.push(package_almost.clone()); - // Add to the partial solution with incompat as cause. - self.partial_solution.add_derivation( - package_almost, - root_cause, - &self.incompatibility_store, - ); - // After conflict resolution and the partial solution update, - // the root cause incompatibility is now contradicted. - self.contradicted_incompatibilities.insert(root_cause); - } - } - // If there are no more changed packages, unit propagation is done. - Ok(()) - } - - /// Return the root cause and the backtracked model. - /// CF - fn conflict_resolution( - &mut self, - incompatibility: IncompId, - ) -> Result<(P, IncompId), PubGrubError> { - let mut current_incompat_id = incompatibility; - let mut current_incompat_changed = false; - loop { - if self.incompatibility_store[current_incompat_id] - .is_terminal(&self.root_package, &self.root_version) - { - return Err(PubGrubError::NoSolution( - self.build_derivation_tree(current_incompat_id), - )); - } else { - let (package, satisfier_search_result) = self.partial_solution.satisfier_search( - &self.incompatibility_store[current_incompat_id], - &self.incompatibility_store, - ); - match satisfier_search_result { - DifferentDecisionLevels { - previous_satisfier_level, - } => { - self.backtrack( - current_incompat_id, - current_incompat_changed, - previous_satisfier_level, - ); - return Ok((package, current_incompat_id)); - } - SameDecisionLevels { satisfier_cause } => { - let prior_cause = Incompatibility::prior_cause( - current_incompat_id, - satisfier_cause, - &package, - &self.incompatibility_store, - ); - current_incompat_id = self.incompatibility_store.alloc(prior_cause); - current_incompat_changed = true; - } - } - } - } - } - - /// Backtracking. - fn backtrack( - &mut self, - incompat: IncompId, - incompat_changed: bool, - decision_level: DecisionLevel, - ) { - self.partial_solution - .backtrack(decision_level, &self.incompatibility_store); - self.contradicted_incompatibilities.clear(); - if incompat_changed { - self.merge_incompatibility(incompat); - } - } - - /// Add this incompatibility into the set of all incompatibilities. - /// - /// Pub collapses identical dependencies from adjacent package versions - /// into individual incompatibilities. - /// This substantially reduces the total number of incompatibilities - /// and makes it much easier for Pub to reason about multiple versions of packages at once. - /// - /// For example, rather than representing - /// foo 1.0.0 depends on bar ^1.0.0 and - /// foo 1.1.0 depends on bar ^1.0.0 - /// as two separate incompatibilities, - /// they are collapsed together into the single incompatibility {foo ^1.0.0, not bar ^1.0.0} - /// (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) { - for (pkg, _term) in self.incompatibility_store[id].iter() { - self.incompatibilities - .entry(pkg.clone()) - .or_default() - .push(id); - } - } - - // Error reporting ######################################################### - - fn build_derivation_tree(&self, incompat: IncompId) -> DerivationTree { - let shared_ids = self.find_shared_ids(incompat); - Incompatibility::build_derivation_tree(incompat, &shared_ids, &self.incompatibility_store) - } - - fn find_shared_ids(&self, incompat: IncompId) -> Set> { - let mut all_ids = Set::new(); - let mut shared_ids = Set::new(); - let mut stack = vec![incompat]; - while let Some(i) = stack.pop() { - if let Some((id1, id2)) = self.incompatibility_store[i].causes() { - if all_ids.contains(&i) { - shared_ids.insert(i); - } else { - all_ids.insert(i); - stack.push(id1); - stack.push(id2); - } - } - } - shared_ids - } -} diff --git a/src/internal/incompatibility.rs b/src/internal/incompatibility.rs index acf900b8..fce89e20 100644 --- a/src/internal/incompatibility.rs +++ b/src/internal/incompatibility.rs @@ -9,10 +9,9 @@ use std::fmt; use crate::internal::arena::{Arena, Id}; use crate::internal::small_map::SmallMap; use crate::package::Package; -use crate::range::Range; use crate::report::{DefaultStringReporter, DerivationTree, Derived, External}; use crate::term::{self, Term}; -use crate::version::Version; +use crate::version_set::VersionSet; /// An incompatibility is a set of terms for different packages /// that should never be satisfied all together. @@ -30,26 +29,26 @@ use crate::version::Version; /// during conflict resolution. More about all this in /// [PubGrub documentation](https://github.com/dart-lang/pub/blob/master/doc/solver.md#incompatibility). #[derive(Debug, Clone)] -pub struct Incompatibility { - package_terms: SmallMap>, - kind: Kind, +pub struct Incompatibility { + package_terms: SmallMap>, + kind: Kind, } /// Type alias of unique identifiers for incompatibilities. -pub type IncompId = Id>; +pub type IncompId = Id>; #[derive(Debug, Clone)] -enum Kind { +enum Kind { /// Initial incompatibility aiming at picking the root package for the first decision. - NotRoot(P, V), + NotRoot(P, VS::V), /// There are no versions in the given range for this package. - NoVersions(P, Range), + NoVersions(P, VS), /// Dependencies of the package are unavailable for versions in that range. - UnavailableDependencies(P, Range), + UnavailableDependencies(P, VS), /// Incompatibility coming from the dependencies of a given package. - FromDependencyOf(P, Range, P, Range), + FromDependencyOf(P, VS, P, VS), /// Derived from two causes. Stores cause ids. - DerivedFrom(IncompId, IncompId), + DerivedFrom(IncompId, IncompId), } /// A Relation describes how a set of terms can be compared to an incompatibility. @@ -69,52 +68,52 @@ pub enum Relation { Inconclusive, } -impl Incompatibility { +impl Incompatibility { /// Create the initial "not Root" incompatibility. - pub fn not_root(package: P, version: V) -> Self { + pub fn not_root(package: P, version: VS::V) -> Self { Self { package_terms: SmallMap::One([( package.clone(), - Term::Negative(Range::exact(version.clone())), + Term::Negative(VS::singleton(version.clone())), )]), kind: Kind::NotRoot(package, version), } } /// Create an incompatibility to remember - /// that a given range does not contain any version. - pub fn no_versions(package: P, term: Term) -> Self { - let range = match &term { + /// that a given set does not contain any version. + pub fn no_versions(package: P, term: Term) -> Self { + let set = match &term { Term::Positive(r) => r.clone(), Term::Negative(_) => panic!("No version should have a positive term"), }; Self { package_terms: SmallMap::One([(package.clone(), term)]), - kind: Kind::NoVersions(package, range), + kind: Kind::NoVersions(package, set), } } /// Create an incompatibility to remember /// that a package version is not selectable /// because its list of dependencies is unavailable. - pub fn unavailable_dependencies(package: P, version: V) -> Self { - let range = Range::exact(version); + pub fn unavailable_dependencies(package: P, version: VS::V) -> Self { + let set = VS::singleton(version); Self { - package_terms: SmallMap::One([(package.clone(), Term::Positive(range.clone()))]), - kind: Kind::UnavailableDependencies(package, range), + package_terms: SmallMap::One([(package.clone(), Term::Positive(set.clone()))]), + kind: Kind::UnavailableDependencies(package, set), } } /// Build an incompatibility from a given dependency. - pub fn from_dependency(package: P, version: V, dep: (&P, &Range)) -> Self { - let range1 = Range::exact(version); - let (p2, range2) = dep; + pub fn from_dependency(package: P, version: VS::V, dep: (&P, &VS)) -> Self { + let set1 = VS::singleton(version); + let (p2, set2) = dep; Self { package_terms: SmallMap::Two([ - (package.clone(), Term::Positive(range1.clone())), - (p2.clone(), Term::Negative(range2.clone())), + (package.clone(), Term::Positive(set1.clone())), + (p2.clone(), Term::Negative(set2.clone())), ]), - kind: Kind::FromDependencyOf(package, range1, p2.clone(), range2.clone()), + kind: Kind::FromDependencyOf(package, set1, p2.clone(), set2.clone()), } } @@ -145,7 +144,7 @@ impl Incompatibility { /// Check if an incompatibility should mark the end of the algorithm /// because it satisfies the root package. - pub fn is_terminal(&self, root_package: &P, root_version: &V) -> bool { + pub fn is_terminal(&self, root_package: &P, root_version: &VS::V) -> bool { if self.package_terms.len() == 0 { true } else if self.package_terms.len() > 1 { @@ -157,12 +156,12 @@ impl Incompatibility { } /// Get the term related to a given package (if it exists). - pub fn get(&self, package: &P) -> Option<&Term> { + pub fn get(&self, package: &P) -> Option<&Term> { self.package_terms.get(package) } /// Iterate over packages. - pub fn iter(&self) -> impl Iterator)> { + pub fn iter(&self) -> impl Iterator)> { self.package_terms.iter() } @@ -181,7 +180,7 @@ impl Incompatibility { self_id: Id, shared_ids: &Set>, store: &Arena, - ) -> DerivationTree { + ) -> DerivationTree { match &store[self_id].kind { Kind::DerivedFrom(id1, id2) => { let cause1 = Self::build_derivation_tree(*id1, shared_ids, store); @@ -197,27 +196,27 @@ impl Incompatibility { Kind::NotRoot(package, version) => { DerivationTree::External(External::NotRoot(package.clone(), version.clone())) } - Kind::NoVersions(package, range) => { - DerivationTree::External(External::NoVersions(package.clone(), range.clone())) + Kind::NoVersions(package, set) => { + DerivationTree::External(External::NoVersions(package.clone(), set.clone())) } - Kind::UnavailableDependencies(package, range) => DerivationTree::External( - External::UnavailableDependencies(package.clone(), range.clone()), + Kind::UnavailableDependencies(package, set) => DerivationTree::External( + External::UnavailableDependencies(package.clone(), set.clone()), ), - Kind::FromDependencyOf(package, range, dep_package, dep_range) => { + Kind::FromDependencyOf(package, set, dep_package, dep_set) => { DerivationTree::External(External::FromDependencyOf( package.clone(), - range.clone(), + set.clone(), dep_package.clone(), - dep_range.clone(), + dep_set.clone(), )) } } } } -impl<'a, P: Package, V: Version + 'a> Incompatibility { +impl<'a, P: Package, VS: VersionSet + 'a> Incompatibility { /// CF definition of Relation enum. - pub fn relation(&self, terms: impl Fn(&P) -> Option<&'a Term>) -> Relation

{ + pub fn relation(&self, terms: impl Fn(&P) -> Option<&'a Term>) -> Relation

{ let mut relation = Relation::Satisfied; for (package, incompat_term) in self.package_terms.iter() { match terms(package).map(|term| incompat_term.relation_with(&term)) { @@ -243,7 +242,7 @@ impl<'a, P: Package, V: Version + 'a> Incompatibility { } } -impl fmt::Display for Incompatibility { +impl fmt::Display for Incompatibility { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -258,6 +257,7 @@ impl fmt::Display for Incompatibility { #[cfg(test)] pub mod tests { use super::*; + use crate::range::Range; use crate::term::tests::strategy as term_strat; use crate::type_aliases::Map; use proptest::prelude::*; diff --git a/src/internal/incompatibility_bis.rs b/src/internal/incompatibility_bis.rs deleted file mode 100644 index 03f67b22..00000000 --- a/src/internal/incompatibility_bis.rs +++ /dev/null @@ -1,296 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! An incompatibility is a set of terms for different packages -//! that should never be satisfied all together. - -use std::collections::HashSet as Set; -use std::fmt; - -use crate::internal::arena::{Arena, Id}; -use crate::internal::small_map::SmallMap; -use crate::package::Package; -use crate::report_bis::{DefaultStringReporter, DerivationTree, Derived, External}; -use crate::term_bis::{self, Term}; -use crate::version_set::VersionSet; - -/// An incompatibility is a set of terms for different packages -/// that should never be satisfied all together. -/// An incompatibility usually originates from a package dependency. -/// For example, if package A at version 1 depends on package B -/// at version 2, you can never have both terms `A = 1` -/// and `not B = 2` satisfied at the same time in a partial solution. -/// This would mean that we found a solution with package A at version 1 -/// but not with package B at version 2. -/// Yet A at version 1 depends on B at version 2 so this is not possible. -/// Therefore, the set `{ A = 1, not B = 2 }` is an incompatibility, -/// defined from dependencies of A at version 1. -/// -/// Incompatibilities can also be derived from two other incompatibilities -/// during conflict resolution. More about all this in -/// [PubGrub documentation](https://github.com/dart-lang/pub/blob/master/doc/solver.md#incompatibility). -#[derive(Debug, Clone)] -pub struct Incompatibility { - package_terms: SmallMap>, - kind: Kind, -} - -/// Type alias of unique identifiers for incompatibilities. -pub type IncompId = Id>; - -#[derive(Debug, Clone)] -enum Kind { - /// Initial incompatibility aiming at picking the root package for the first decision. - NotRoot(P, VS::V), - /// There are no versions in the given range for this package. - NoVersions(P, VS), - /// Dependencies of the package are unavailable for versions in that range. - UnavailableDependencies(P, VS), - /// Incompatibility coming from the dependencies of a given package. - FromDependencyOf(P, VS, P, VS), - /// Derived from two causes. Stores cause ids. - DerivedFrom(IncompId, IncompId), -} - -/// A Relation describes how a set of terms can be compared to an incompatibility. -/// Typically, the set of terms comes from the partial solution. -#[derive(Eq, PartialEq, Debug)] -pub enum Relation { - /// We say that a set of terms S satisfies an incompatibility I - /// if S satisfies every term in I. - Satisfied, - /// We say that S contradicts I - /// if S contradicts at least one term in I. - Contradicted(P), - /// If S satisfies all but one of I's terms and is inconclusive for the remaining term, - /// we say S "almost satisfies" I and we call the remaining term the "unsatisfied term". - AlmostSatisfied(P), - /// Otherwise, we say that their relation is inconclusive. - Inconclusive, -} - -impl Incompatibility { - /// Create the initial "not Root" incompatibility. - pub fn not_root(package: P, version: VS::V) -> Self { - Self { - package_terms: SmallMap::One([( - package.clone(), - Term::Negative(VS::singleton(version.clone())), - )]), - kind: Kind::NotRoot(package, version), - } - } - - /// Create an incompatibility to remember - /// that a given set does not contain any version. - pub fn no_versions(package: P, term: Term) -> Self { - let set = match &term { - Term::Positive(r) => r.clone(), - Term::Negative(_) => panic!("No version should have a positive term"), - }; - Self { - package_terms: SmallMap::One([(package.clone(), term)]), - kind: Kind::NoVersions(package, set), - } - } - - /// Create an incompatibility to remember - /// that a package version is not selectable - /// because its list of dependencies is unavailable. - pub fn unavailable_dependencies(package: P, version: VS::V) -> Self { - let set = VS::singleton(version); - Self { - package_terms: SmallMap::One([(package.clone(), Term::Positive(set.clone()))]), - kind: Kind::UnavailableDependencies(package, set), - } - } - - /// 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); - let (p2, set2) = dep; - Self { - package_terms: SmallMap::Two([ - (package.clone(), Term::Positive(set1.clone())), - (p2.clone(), Term::Negative(set2.clone())), - ]), - kind: Kind::FromDependencyOf(package, set1, p2.clone(), set2.clone()), - } - } - - /// Prior cause of two incompatibilities using the rule of resolution. - pub fn prior_cause( - incompat: Id, - satisfier_cause: Id, - package: &P, - incompatibility_store: &Arena, - ) -> Self { - let kind = Kind::DerivedFrom(incompat, satisfier_cause); - let mut package_terms = incompatibility_store[incompat].package_terms.clone(); - let t1 = package_terms.remove(package).unwrap(); - let satisfier_cause_terms = &incompatibility_store[satisfier_cause].package_terms; - package_terms.merge( - satisfier_cause_terms.iter().filter(|(p, _)| p != &package), - |t1, t2| Some(t1.intersection(t2)), - ); - let term = t1.union(satisfier_cause_terms.get(package).unwrap()); - if term != Term::any() { - package_terms.insert(package.clone(), term); - } - Self { - package_terms, - kind, - } - } - - /// Check if an incompatibility should mark the end of the algorithm - /// because it satisfies the root package. - pub fn is_terminal(&self, root_package: &P, root_version: &VS::V) -> bool { - if self.package_terms.len() == 0 { - true - } else if self.package_terms.len() > 1 { - false - } else { - let (package, term) = self.package_terms.iter().next().unwrap(); - (package == root_package) && term.contains(&root_version) - } - } - - /// Get the term related to a given package (if it exists). - pub fn get(&self, package: &P) -> Option<&Term> { - self.package_terms.get(package) - } - - /// Iterate over packages. - pub fn iter(&self) -> impl Iterator)> { - self.package_terms.iter() - } - - // Reporting ############################################################### - - /// Retrieve parent causes if of type DerivedFrom. - pub fn causes(&self) -> Option<(Id, Id)> { - match self.kind { - Kind::DerivedFrom(id1, id2) => Some((id1, id2)), - _ => None, - } - } - - /// Build a derivation tree for error reporting. - pub fn build_derivation_tree( - self_id: Id, - shared_ids: &Set>, - store: &Arena, - ) -> DerivationTree { - match &store[self_id].kind { - Kind::DerivedFrom(id1, id2) => { - let cause1 = Self::build_derivation_tree(*id1, shared_ids, store); - let cause2 = Self::build_derivation_tree(*id2, shared_ids, store); - let derived = Derived { - terms: store[self_id].package_terms.as_map(), - shared_id: shared_ids.get(&self_id).map(|id| id.into_raw()), - cause1: Box::new(cause1), - cause2: Box::new(cause2), - }; - DerivationTree::Derived(derived) - } - Kind::NotRoot(package, version) => { - DerivationTree::External(External::NotRoot(package.clone(), version.clone())) - } - Kind::NoVersions(package, set) => { - DerivationTree::External(External::NoVersions(package.clone(), set.clone())) - } - Kind::UnavailableDependencies(package, set) => DerivationTree::External( - External::UnavailableDependencies(package.clone(), set.clone()), - ), - Kind::FromDependencyOf(package, set, dep_package, dep_set) => { - DerivationTree::External(External::FromDependencyOf( - package.clone(), - set.clone(), - dep_package.clone(), - dep_set.clone(), - )) - } - } - } -} - -impl<'a, P: Package, VS: VersionSet + 'a> Incompatibility { - /// CF definition of Relation enum. - pub fn relation(&self, terms: impl Fn(&P) -> Option<&'a Term>) -> Relation

{ - let mut relation = Relation::Satisfied; - for (package, incompat_term) in self.package_terms.iter() { - match terms(package).map(|term| incompat_term.relation_with(&term)) { - Some(term_bis::Relation::Satisfied) => {} - Some(term_bis::Relation::Contradicted) => { - return Relation::Contradicted(package.clone()); - } - None | Some(term_bis::Relation::Inconclusive) => { - // If a package is not present, the intersection is the same as [Term::any]. - // According to the rules of satisfactions, the relation would be inconclusive. - // It could also be satisfied if the incompatibility term was also [Term::any], - // but we systematically remove those from incompatibilities - // so we're safe on that front. - if relation == Relation::Satisfied { - relation = Relation::AlmostSatisfied(package.clone()); - } else { - relation = Relation::Inconclusive; - } - } - } - } - relation - } -} - -impl fmt::Display for Incompatibility { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{}", - DefaultStringReporter::string_terms(&self.package_terms.as_map()) - ) - } -} - -// TESTS ####################################################################### - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::range::Range; - use crate::term_bis::tests::strategy as term_strat; - use crate::type_aliases::Map; - use proptest::prelude::*; - - proptest! { - - /// For any three different packages p1, p2 and p3, - /// for any three terms t1, t2 and t3, - /// if we have the two following incompatibilities: - /// { p1: t1, p2: not t2 } - /// { p2: t2, p3: t3 } - /// the rule of resolution says that we can deduce the following incompatibility: - /// { p1: t1, p3: t3 } - #[test] - fn rule_of_resolution(t1 in term_strat(), t2 in term_strat(), t3 in term_strat()) { - let mut store = Arena::new(); - let i1 = store.alloc(Incompatibility { - package_terms: SmallMap::Two([("p1", t1.clone()), ("p2", t2.negate())]), - kind: Kind::UnavailableDependencies("0", Range::any()) - }); - - let i2 = store.alloc(Incompatibility { - package_terms: SmallMap::Two([("p2", t2), ("p3", t3.clone())]), - kind: Kind::UnavailableDependencies("0", Range::any()) - }); - - let mut i3 = Map::default(); - i3.insert("p1", t1); - i3.insert("p3", t3); - - let i_resolution = Incompatibility::prior_cause(i1, i2, &"p2", &store); - assert_eq!(i_resolution.package_terms.as_map(), i3); - } - - } -} diff --git a/src/internal/mod.rs b/src/internal/mod.rs index 808819ea..86d7e22e 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -4,10 +4,7 @@ pub mod arena; pub mod core; -pub mod core_bis; pub mod incompatibility; -pub mod incompatibility_bis; pub mod partial_solution; -pub mod partial_solution_bis; pub mod small_map; pub mod small_vec; diff --git a/src/internal/partial_solution.rs b/src/internal/partial_solution.rs index 84bfc5f8..2291591a 100644 --- a/src/internal/partial_solution.rs +++ b/src/internal/partial_solution.rs @@ -7,10 +7,9 @@ use crate::internal::arena::Arena; use crate::internal::incompatibility::{IncompId, Incompatibility, Relation}; use crate::internal::small_map::SmallMap; use crate::package::Package; -use crate::range::Range; use crate::term::Term; use crate::type_aliases::{Map, SelectedDependencies}; -use crate::version::Version; +use crate::version_set::VersionSet; use super::small_vec::SmallVec; @@ -26,47 +25,47 @@ impl DecisionLevel { /// The partial solution contains all package assignments, /// organized by package and historically ordered. #[derive(Clone, Debug)] -pub struct PartialSolution { +pub struct PartialSolution { next_global_index: u32, current_decision_level: DecisionLevel, - package_assignments: Map>, + package_assignments: Map>, } /// Package assignments contain the potential decision and derivations /// that have already been made for a given package, /// as well as the intersection of terms by all of these. #[derive(Clone, Debug)] -struct PackageAssignments { +struct PackageAssignments { smallest_decision_level: DecisionLevel, highest_decision_level: DecisionLevel, - dated_derivations: SmallVec>, - assignments_intersection: AssignmentsIntersection, + dated_derivations: SmallVec>, + assignments_intersection: AssignmentsIntersection, } #[derive(Clone, Debug)] -pub struct DatedDerivation { +pub struct DatedDerivation { global_index: u32, decision_level: DecisionLevel, - cause: IncompId, + cause: IncompId, } #[derive(Clone, Debug)] -enum AssignmentsIntersection { - Decision((u32, V, Term)), - Derivations(Term), +enum AssignmentsIntersection { + Decision((u32, VS::V, Term)), + Derivations(Term), } #[derive(Clone, Debug)] -pub enum SatisfierSearch { +pub enum SatisfierSearch { DifferentDecisionLevels { previous_satisfier_level: DecisionLevel, }, SameDecisionLevels { - satisfier_cause: IncompId, + satisfier_cause: IncompId, }, } -impl PartialSolution { +impl PartialSolution { /// Initialize an empty PartialSolution. pub fn empty() -> Self { Self { @@ -77,7 +76,7 @@ impl PartialSolution { } /// Add a decision. - pub fn add_decision(&mut self, package: P, version: V) { + pub fn add_decision(&mut self, package: P, version: VS::V) { // Check that add_decision is never used in the wrong context. if cfg!(debug_assertions) { match self.package_assignments.get_mut(&package) { @@ -110,8 +109,8 @@ impl PartialSolution { pub fn add_derivation( &mut self, package: P, - cause: IncompId, - store: &Arena>, + cause: IncompId, + store: &Arena>, ) { use std::collections::hash_map::Entry; let term = store[cause].get(&package).unwrap().negate(); @@ -153,7 +152,7 @@ impl PartialSolution { /// selected version (no "decision") /// and if it contains at least one positive derivation term /// in the partial solution. - pub fn potential_packages(&self) -> Option)>> { + pub fn potential_packages(&self) -> Option> { let mut iter = self .package_assignments .iter() @@ -169,7 +168,7 @@ impl PartialSolution { /// If a partial solution has, for every positive derivation, /// a corresponding decision that satisfies that assignment, /// it's a total solution and version solving has succeeded. - pub fn extract_solution(&self) -> Option> { + pub fn extract_solution(&self) -> Option> { let mut solution = Map::default(); for (p, pa) in &self.package_assignments { match &pa.assignments_intersection { @@ -190,7 +189,7 @@ impl PartialSolution { pub fn backtrack( &mut self, decision_level: DecisionLevel, - store: &Arena>, + store: &Arena>, ) { self.current_decision_level = decision_level; self.package_assignments.retain(|p, pa| { @@ -240,12 +239,12 @@ impl PartialSolution { pub fn add_version( &mut self, package: P, - version: V, - new_incompatibilities: std::ops::Range>, - store: &Arena>, + version: VS::V, + new_incompatibilities: std::ops::Range>, + store: &Arena>, ) { let exact = Term::exact(version.clone()); - let not_satisfied = |incompat: &Incompatibility| { + let not_satisfied = |incompat: &Incompatibility| { incompat.relation(|p| { if p == &package { Some(&exact) @@ -263,12 +262,12 @@ impl PartialSolution { } /// Check if the terms in the partial solution satisfy the incompatibility. - pub fn relation(&self, incompat: &Incompatibility) -> Relation

{ + pub fn relation(&self, incompat: &Incompatibility) -> Relation

{ incompat.relation(|package| self.term_intersection_for_package(package)) } /// Retrieve intersection of terms related to package. - pub fn term_intersection_for_package(&self, package: &P) -> Option<&Term> { + pub fn term_intersection_for_package(&self, package: &P) -> Option<&Term> { self.package_assignments .get(package) .map(|pa| pa.assignments_intersection.term()) @@ -277,9 +276,9 @@ impl PartialSolution { /// Figure out if the satisfier and previous satisfier are of different decision levels. pub fn satisfier_search( &self, - incompat: &Incompatibility, - store: &Arena>, - ) -> (P, SatisfierSearch) { + incompat: &Incompatibility, + store: &Arena>, + ) -> (P, SatisfierSearch) { let satisfied_map = Self::find_satisfier(incompat, &self.package_assignments, store); let (satisfier_package, &(satisfier_index, _, satisfier_decision_level)) = satisfied_map .iter() @@ -318,9 +317,9 @@ impl PartialSolution { /// It would be nice if we could get rid of it, but I don't know if then it will be possible /// to return a coherent previous_satisfier_level. fn find_satisfier( - incompat: &Incompatibility, - package_assignments: &Map>, - store: &Arena>, + incompat: &Incompatibility, + package_assignments: &Map>, + store: &Arena>, ) -> SmallMap { let mut satisfied = SmallMap::Empty; for (package, incompat_term) in incompat.iter() { @@ -337,11 +336,11 @@ impl PartialSolution { /// such that incompatibility is satisfied by the partial solution up to /// and including that assignment plus satisfier. fn find_previous_satisfier( - incompat: &Incompatibility, + incompat: &Incompatibility, satisfier_package: &P, mut satisfied_map: SmallMap, - package_assignments: &Map>, - store: &Arena>, + package_assignments: &Map>, + store: &Arena>, ) -> DecisionLevel { // First, let's retrieve the previous derivations and the initial accum_term. let satisfier_pa = package_assignments.get(satisfier_package).unwrap(); @@ -375,13 +374,13 @@ impl PartialSolution { } } -impl PackageAssignments { +impl PackageAssignments { fn satisfier( &self, package: &P, - incompat_term: &Term, - start_term: Term, - store: &Arena>, + incompat_term: &Term, + start_term: Term, + store: &Arena>, ) -> (usize, u32, DecisionLevel) { // Term where we accumulate intersections until incompat_term is satisfied. let mut accum_term = start_term; @@ -422,9 +421,9 @@ impl PackageAssignments { } } -impl AssignmentsIntersection { +impl AssignmentsIntersection { /// Returns the term intersection of all assignments (decision included). - fn term(&self) -> &Term { + fn term(&self) -> &Term { match self { Self::Decision((_, _, term)) => term, Self::Derivations(term) => term, @@ -438,7 +437,7 @@ impl AssignmentsIntersection { fn potential_package_filter<'a, P: Package>( &'a self, package: &'a P, - ) -> Option<(&'a P, &'a Range)> { + ) -> Option<(&'a P, &'a VS)> { match self { Self::Decision(_) => None, Self::Derivations(term_intersection) => { diff --git a/src/internal/partial_solution_bis.rs b/src/internal/partial_solution_bis.rs deleted file mode 100644 index 20499db1..00000000 --- a/src/internal/partial_solution_bis.rs +++ /dev/null @@ -1,452 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! A Memory acts like a structured partial solution -//! where terms are regrouped by package in a [Map](crate::type_aliases::Map). - -use crate::internal::arena::Arena; -use crate::internal::incompatibility_bis::{IncompId, Incompatibility, Relation}; -use crate::internal::small_map::SmallMap; -use crate::package::Package; -use crate::term_bis::Term; -use crate::type_aliases::{Map, SelectedDependencies}; -use crate::version_set::VersionSet; - -use super::small_vec::SmallVec; - -#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] -pub struct DecisionLevel(pub u32); - -impl DecisionLevel { - pub fn increment(self) -> Self { - Self(self.0 + 1) - } -} - -/// The partial solution contains all package assignments, -/// organized by package and historically ordered. -#[derive(Clone, Debug)] -pub struct PartialSolution { - next_global_index: u32, - current_decision_level: DecisionLevel, - package_assignments: Map>, -} - -/// Package assignments contain the potential decision and derivations -/// that have already been made for a given package, -/// as well as the intersection of terms by all of these. -#[derive(Clone, Debug)] -struct PackageAssignments { - smallest_decision_level: DecisionLevel, - highest_decision_level: DecisionLevel, - dated_derivations: SmallVec>, - assignments_intersection: AssignmentsIntersection, -} - -#[derive(Clone, Debug)] -pub struct DatedDerivation { - global_index: u32, - decision_level: DecisionLevel, - cause: IncompId, -} - -#[derive(Clone, Debug)] -enum AssignmentsIntersection { - Decision((u32, VS::V, Term)), - Derivations(Term), -} - -#[derive(Clone, Debug)] -pub enum SatisfierSearch { - DifferentDecisionLevels { - previous_satisfier_level: DecisionLevel, - }, - SameDecisionLevels { - satisfier_cause: IncompId, - }, -} - -impl PartialSolution { - /// Initialize an empty PartialSolution. - pub fn empty() -> Self { - Self { - next_global_index: 0, - current_decision_level: DecisionLevel(0), - package_assignments: Map::default(), - } - } - - /// Add a decision. - pub fn add_decision(&mut self, package: P, version: VS::V) { - // Check that add_decision is never used in the wrong context. - if cfg!(debug_assertions) { - match self.package_assignments.get_mut(&package) { - None => panic!("Derivations must already exist"), - Some(pa) => match &pa.assignments_intersection { - // Cannot be called when a decision has already been taken. - AssignmentsIntersection::Decision(_) => panic!("Already existing decision"), - // Cannot be called if the versions is not contained in the terms intersection. - AssignmentsIntersection::Derivations(term) => { - debug_assert!(term.contains(&version)) - } - }, - } - } - self.current_decision_level = self.current_decision_level.increment(); - let mut pa = self - .package_assignments - .get_mut(&package) - .expect("Derivations must already exist"); - pa.highest_decision_level = self.current_decision_level; - pa.assignments_intersection = AssignmentsIntersection::Decision(( - self.next_global_index, - version.clone(), - Term::exact(version), - )); - self.next_global_index += 1; - } - - /// Add a derivation. - pub fn add_derivation( - &mut self, - package: P, - cause: IncompId, - store: &Arena>, - ) { - use std::collections::hash_map::Entry; - let term = store[cause].get(&package).unwrap().negate(); - let dated_derivation = DatedDerivation { - global_index: self.next_global_index, - decision_level: self.current_decision_level, - cause, - }; - self.next_global_index += 1; - match self.package_assignments.entry(package) { - Entry::Occupied(mut occupied) => { - let mut pa = occupied.get_mut(); - pa.highest_decision_level = self.current_decision_level; - match &mut pa.assignments_intersection { - // Check that add_derivation is never called in the wrong context. - AssignmentsIntersection::Decision(_) => { - panic!("add_derivation should not be called after a decision") - } - AssignmentsIntersection::Derivations(t) => { - *t = t.intersection(&term); - } - } - pa.dated_derivations.push(dated_derivation); - } - Entry::Vacant(v) => { - v.insert(PackageAssignments { - smallest_decision_level: self.current_decision_level, - highest_decision_level: self.current_decision_level, - dated_derivations: SmallVec::One([dated_derivation]), - assignments_intersection: AssignmentsIntersection::Derivations(term), - }); - } - } - } - - /// Extract potential packages for the next iteration of unit propagation. - /// Return `None` if there is no suitable package anymore, which stops the algorithm. - /// A package is a potential pick if there isn't an already - /// selected version (no "decision") - /// and if it contains at least one positive derivation term - /// in the partial solution. - pub fn potential_packages(&self) -> Option> { - let mut iter = self - .package_assignments - .iter() - .filter_map(|(p, pa)| pa.assignments_intersection.potential_package_filter(p)) - .peekable(); - if iter.peek().is_some() { - Some(iter) - } else { - None - } - } - - /// If a partial solution has, for every positive derivation, - /// a corresponding decision that satisfies that assignment, - /// it's a total solution and version solving has succeeded. - pub fn extract_solution(&self) -> Option> { - let mut solution = Map::default(); - for (p, pa) in &self.package_assignments { - match &pa.assignments_intersection { - AssignmentsIntersection::Decision((_, v, _)) => { - solution.insert(p.clone(), v.clone()); - } - AssignmentsIntersection::Derivations(term) => { - if term.is_positive() { - return None; - } - } - } - } - Some(solution) - } - - /// Backtrack the partial solution to a given decision level. - pub fn backtrack( - &mut self, - decision_level: DecisionLevel, - store: &Arena>, - ) { - self.current_decision_level = decision_level; - self.package_assignments.retain(|p, pa| { - if pa.smallest_decision_level > decision_level { - // Remove all entries that have a smallest decision level higher than the backtrack target. - false - } else if pa.highest_decision_level <= decision_level { - // Do not change entries older than the backtrack decision level target. - true - } else { - // smallest_decision_level <= decision_level < highest_decision_level - // - // Since decision_level < highest_decision_level, - // We can be certain that there will be no decision in this package assignments - // after backtracking, because such decision would have been the last - // assignment and it would have the "highest_decision_level". - - // Truncate the history. - while pa.dated_derivations.last().map(|dd| dd.decision_level) > Some(decision_level) - { - pa.dated_derivations.pop(); - } - debug_assert!(!pa.dated_derivations.is_empty()); - - // Update highest_decision_level. - pa.highest_decision_level = pa.dated_derivations.last().unwrap().decision_level; - - // Recompute the assignments intersection. - pa.assignments_intersection = AssignmentsIntersection::Derivations( - pa.dated_derivations - .iter() - .fold(Term::any(), |acc, dated_derivation| { - let term = store[dated_derivation.cause].get(&p).unwrap().negate(); - acc.intersection(&term) - }), - ); - 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 fn add_version( - &mut self, - package: P, - version: VS::V, - new_incompatibilities: std::ops::Range>, - store: &Arena>, - ) { - let exact = Term::exact(version.clone()); - let not_satisfied = |incompat: &Incompatibility| { - incompat.relation(|p| { - if p == &package { - Some(&exact) - } else { - self.term_intersection_for_package(p) - } - }) != Relation::Satisfied - }; - - // Check none of the dependencies (new_incompatibilities) - // would create a conflict (be satisfied). - if store[new_incompatibilities].iter().all(not_satisfied) { - self.add_decision(package, version); - } - } - - /// Check if the terms in the partial solution satisfy the incompatibility. - pub fn relation(&self, incompat: &Incompatibility) -> Relation

{ - incompat.relation(|package| self.term_intersection_for_package(package)) - } - - /// Retrieve intersection of terms related to package. - pub fn term_intersection_for_package(&self, package: &P) -> Option<&Term> { - self.package_assignments - .get(package) - .map(|pa| pa.assignments_intersection.term()) - } - - /// Figure out if the satisfier and previous satisfier are of different decision levels. - pub fn satisfier_search( - &self, - incompat: &Incompatibility, - store: &Arena>, - ) -> (P, SatisfierSearch) { - let satisfied_map = Self::find_satisfier(incompat, &self.package_assignments, store); - let (satisfier_package, &(satisfier_index, _, satisfier_decision_level)) = satisfied_map - .iter() - .max_by_key(|(_p, (_, global_index, _))| global_index) - .unwrap(); - let satisfier_package = satisfier_package.clone(); - let previous_satisfier_level = Self::find_previous_satisfier( - incompat, - &satisfier_package, - satisfied_map, - &self.package_assignments, - store, - ); - if previous_satisfier_level < satisfier_decision_level { - let search_result = SatisfierSearch::DifferentDecisionLevels { - previous_satisfier_level, - }; - (satisfier_package, search_result) - } else { - let satisfier_pa = self.package_assignments.get(&satisfier_package).unwrap(); - let dd = &satisfier_pa.dated_derivations[satisfier_index]; - let search_result = SatisfierSearch::SameDecisionLevels { - satisfier_cause: dd.cause, - }; - (satisfier_package, search_result) - } - } - - /// A satisfier is the earliest assignment in partial solution such that the incompatibility - /// is satisfied by the partial solution up to and including that assignment. - /// - /// Returns a map indicating for each package term, when that was first satisfied in history. - /// If we effectively found a satisfier, the returned map must be the same size that incompat. - /// - /// Question: This is possible since we added a "global_index" to every dated_derivation. - /// It would be nice if we could get rid of it, but I don't know if then it will be possible - /// to return a coherent previous_satisfier_level. - fn find_satisfier( - incompat: &Incompatibility, - package_assignments: &Map>, - store: &Arena>, - ) -> SmallMap { - let mut satisfied = SmallMap::Empty; - for (package, incompat_term) in incompat.iter() { - let pa = package_assignments.get(package).expect("Must exist"); - satisfied.insert( - package.clone(), - pa.satisfier(package, incompat_term, Term::any(), store), - ); - } - satisfied - } - - /// Earliest assignment in the partial solution before satisfier - /// such that incompatibility is satisfied by the partial solution up to - /// and including that assignment plus satisfier. - fn find_previous_satisfier( - incompat: &Incompatibility, - satisfier_package: &P, - mut satisfied_map: SmallMap, - package_assignments: &Map>, - store: &Arena>, - ) -> DecisionLevel { - // First, let's retrieve the previous derivations and the initial accum_term. - let satisfier_pa = package_assignments.get(satisfier_package).unwrap(); - let (satisfier_index, _gidx, _dl) = satisfied_map.get_mut(satisfier_package).unwrap(); - - let accum_term = if *satisfier_index == satisfier_pa.dated_derivations.len() { - match &satisfier_pa.assignments_intersection { - AssignmentsIntersection::Derivations(_) => panic!("must be a decision"), - AssignmentsIntersection::Decision((_, _, term)) => term.clone(), - } - } else { - let dd = &satisfier_pa.dated_derivations[*satisfier_index]; - store[dd.cause].get(satisfier_package).unwrap().negate() - }; - - let incompat_term = incompat - .get(satisfier_package) - .expect("satisfier package not in incompat"); - - satisfied_map.insert( - satisfier_package.clone(), - satisfier_pa.satisfier(satisfier_package, incompat_term, accum_term, store), - ); - - // Finally, let's identify the decision level of that previous satisfier. - let (_, &(_, _, decision_level)) = satisfied_map - .iter() - .max_by_key(|(_p, (_, global_index, _))| global_index) - .unwrap(); - decision_level.max(DecisionLevel(1)) - } -} - -impl PackageAssignments { - fn satisfier( - &self, - package: &P, - incompat_term: &Term, - start_term: Term, - store: &Arena>, - ) -> (usize, u32, DecisionLevel) { - // Term where we accumulate intersections until incompat_term is satisfied. - let mut accum_term = start_term; - // Indicate if we found a satisfier in the list of derivations, otherwise it will be the decision. - for (idx, dated_derivation) in self.dated_derivations.iter().enumerate() { - let this_term = store[dated_derivation.cause].get(package).unwrap().negate(); - accum_term = accum_term.intersection(&this_term); - if accum_term.subset_of(incompat_term) { - // We found the derivation causing satisfaction. - return ( - idx, - dated_derivation.global_index, - dated_derivation.decision_level, - ); - } - } - // If it wasn't found in the derivations, - // it must be the decision which is last (if called in the right context). - match self.assignments_intersection { - AssignmentsIntersection::Decision((global_index, _, _)) => ( - self.dated_derivations.len(), - global_index, - self.highest_decision_level, - ), - AssignmentsIntersection::Derivations(_) => { - unreachable!( - concat!( - "while processing package {}: ", - "accum_term = {} isn't a subset of incompat_term = {}, ", - "which means the last assignment should have been a decision, ", - "but instead it was a derivation. This shouldn't be possible! ", - "(Maybe your Version ordering is broken?)" - ), - package, accum_term, incompat_term - ) - } - } - } -} - -impl AssignmentsIntersection { - /// Returns the term intersection of all assignments (decision included). - fn term(&self) -> &Term { - match self { - Self::Decision((_, _, term)) => term, - Self::Derivations(term) => term, - } - } - - /// A package is a potential pick if there isn't an already - /// selected version (no "decision") - /// and if it contains at least one positive derivation term - /// in the partial solution. - fn potential_package_filter<'a, P: Package>( - &'a self, - package: &'a P, - ) -> Option<(&'a P, &'a VS)> { - match self { - Self::Decision(_) => None, - Self::Derivations(term_intersection) => { - if term_intersection.is_positive() { - Some((package, term_intersection.unwrap_positive())) - } else { - None - } - } - } - } -} diff --git a/src/lib.rs b/src/lib.rs index 85670342..b0da98b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -210,17 +210,12 @@ #![warn(missing_docs)] pub mod error; -pub mod error_bis; pub mod package; pub mod range; pub mod report; -pub mod report_bis; pub mod solver; -pub mod solver_bis; pub mod term; -pub mod term_bis; pub mod type_aliases; -pub mod type_aliases_bis; pub mod version; pub mod version_set; diff --git a/src/report.rs b/src/report.rs index 07dec364..94db9c3c 100644 --- a/src/report.rs +++ b/src/report.rs @@ -7,50 +7,49 @@ use std::fmt; use std::ops::{Deref, DerefMut}; use crate::package::Package; -use crate::range::Range; use crate::term::Term; use crate::type_aliases::Map; -use crate::version::Version; +use crate::version_set::VersionSet; /// Reporter trait. -pub trait Reporter { +pub trait Reporter { /// Output type of the report. type Output; /// Generate a report from the derivation tree /// describing the resolution failure. - fn report(derivation_tree: &DerivationTree) -> Self::Output; + fn report(derivation_tree: &DerivationTree) -> Self::Output; } /// Derivation tree resulting in the impossibility /// to solve the dependencies of our root package. #[derive(Debug, Clone)] -pub enum DerivationTree { +pub enum DerivationTree { /// External incompatibility. - External(External), + External(External), /// Incompatibility derived from two others. - Derived(Derived), + Derived(Derived), } /// Incompatibilities that are not derived from others, /// they have their own reason. #[derive(Debug, Clone)] -pub enum External { +pub enum External { /// Initial incompatibility aiming at picking the root package for the first decision. - NotRoot(P, V), - /// There are no versions in the given range for this package. - NoVersions(P, Range), - /// Dependencies of the package are unavailable for versions in that range. - UnavailableDependencies(P, Range), + NotRoot(P, VS::V), + /// There are no versions in the given set for this package. + NoVersions(P, VS), + /// Dependencies of the package are unavailable for versions in that set. + UnavailableDependencies(P, VS), /// Incompatibility coming from the dependencies of a given package. - FromDependencyOf(P, Range, P, Range), + FromDependencyOf(P, VS, P, VS), } /// Incompatibility derived from two others. #[derive(Debug, Clone)] -pub struct Derived { +pub struct Derived { /// Terms of the incompatibility. - pub terms: Map>, + pub terms: Map>, /// Indicate if that incompatibility is present multiple times /// in the derivation tree. /// If that is the case, it has a unique id, provided in that option. @@ -58,12 +57,12 @@ pub struct Derived { /// and refer to the explanation for the other times. pub shared_id: Option, /// First cause. - pub cause1: Box>, + pub cause1: Box>, /// Second cause. - pub cause2: Box>, + pub cause2: Box>, } -impl DerivationTree { +impl DerivationTree { /// Merge the [NoVersions](External::NoVersions) external incompatibilities /// with the other one they are matched with /// in a derived incompatibility. @@ -100,7 +99,7 @@ impl DerivationTree { } } - fn merge_no_versions(self, package: P, range: Range) -> Option { + fn merge_no_versions(self, package: P, set: VS) -> Option { match self { // TODO: take care of the Derived case. // Once done, we can remove the Option. @@ -109,19 +108,16 @@ impl DerivationTree { panic!("How did we end up with a NoVersions merged with a NotRoot?") } DerivationTree::External(External::NoVersions(_, r)) => Some(DerivationTree::External( - External::NoVersions(package, range.union(&r)), + External::NoVersions(package, set.union(&r)), )), - DerivationTree::External(External::UnavailableDependencies(_, r)) => { - Some(DerivationTree::External(External::UnavailableDependencies( - package, - range.union(&r), - ))) - } + DerivationTree::External(External::UnavailableDependencies(_, r)) => Some( + DerivationTree::External(External::UnavailableDependencies(package, set.union(&r))), + ), DerivationTree::External(External::FromDependencyOf(p1, r1, p2, r2)) => { if p1 == package { Some(DerivationTree::External(External::FromDependencyOf( p1, - r1.union(&range), + r1.union(&set), p2, r2, ))) @@ -130,7 +126,7 @@ impl DerivationTree { p1, r1, p2, - r2.union(&range), + r2.union(&set), ))) } } @@ -138,39 +134,39 @@ impl DerivationTree { } } -impl fmt::Display for External { +impl fmt::Display for External { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::NotRoot(package, version) => { write!(f, "we are solving dependencies of {} {}", package, version) } - Self::NoVersions(package, range) => { - if range == &Range::any() { + Self::NoVersions(package, set) => { + if set == &VS::full() { write!(f, "there is no available version for {}", package) } else { - write!(f, "there is no version of {} in {}", package, range) + write!(f, "there is no version of {} in {}", package, set) } } - Self::UnavailableDependencies(package, range) => { - if range == &Range::any() { + Self::UnavailableDependencies(package, set) => { + if set == &VS::full() { write!(f, "dependencies of {} are unavailable", package) } else { write!( f, "dependencies of {} at version {} are unavailable", - package, range + package, set ) } } - Self::FromDependencyOf(p, range_p, dep, range_dep) => { - if range_p == &Range::any() && range_dep == &Range::any() { + Self::FromDependencyOf(p, set_p, dep, set_dep) => { + if set_p == &VS::full() && set_dep == &VS::full() { write!(f, "{} depends on {}", p, dep) - } else if range_p == &Range::any() { - write!(f, "{} depends on {} {}", p, dep, range_dep) - } else if range_dep == &Range::any() { - write!(f, "{} {} depends on {}", p, range_p, dep) + } else if set_p == &VS::full() { + write!(f, "{} depends on {} {}", p, dep, set_dep) + } else if set_dep == &VS::full() { + write!(f, "{} {} depends on {}", p, set_p, dep) } else { - write!(f, "{} {} depends on {} {}", p, range_p, dep, range_dep) + write!(f, "{} {} depends on {} {}", p, set_p, dep, set_dep) } } } @@ -198,7 +194,7 @@ impl DefaultStringReporter { } } - fn build_recursive(&mut self, derived: &Derived) { + fn build_recursive(&mut self, derived: &Derived) { self.build_recursive_helper(derived); if let Some(id) = derived.shared_id { if self.shared_with_ref.get(&id) == None { @@ -208,7 +204,7 @@ impl DefaultStringReporter { }; } - fn build_recursive_helper(&mut self, current: &Derived) { + fn build_recursive_helper(&mut self, current: &Derived) { match (current.cause1.deref(), current.cause2.deref()) { (DerivationTree::External(external1), DerivationTree::External(external2)) => { // Simplest case, we just combine two external incompatibilities. @@ -285,11 +281,11 @@ impl DefaultStringReporter { /// /// The result will depend on the fact that the derived incompatibility /// has already been explained or not. - fn report_one_each( + fn report_one_each( &mut self, - derived: &Derived, - external: &External, - current_terms: &Map>, + derived: &Derived, + external: &External, + current_terms: &Map>, ) { match self.line_ref_of(derived.shared_id) { Some(ref_id) => self.lines.push(Self::explain_ref_and_external( @@ -303,11 +299,11 @@ impl DefaultStringReporter { } /// Report one derived (without a line ref yet) and one external. - fn report_recurse_one_each( + fn report_recurse_one_each( &mut self, - derived: &Derived, - external: &External, - current_terms: &Map>, + derived: &Derived, + external: &External, + current_terms: &Map>, ) { match (derived.cause1.deref(), derived.cause2.deref()) { // If the derived cause has itself one external prior cause, @@ -341,10 +337,10 @@ impl DefaultStringReporter { // String explanations ##################################################### /// Simplest case, we just combine two external incompatibilities. - fn explain_both_external( - external1: &External, - external2: &External, - current_terms: &Map>, + fn explain_both_external( + external1: &External, + external2: &External, + current_terms: &Map>, ) -> String { // TODO: order should be chosen to make it more logical. format!( @@ -356,12 +352,12 @@ impl DefaultStringReporter { } /// Both causes have already been explained so we use their refs. - fn explain_both_ref( + fn explain_both_ref( ref_id1: usize, - derived1: &Derived, + derived1: &Derived, ref_id2: usize, - derived2: &Derived, - current_terms: &Map>, + derived2: &Derived, + current_terms: &Map>, ) -> String { // TODO: order should be chosen to make it more logical. format!( @@ -377,11 +373,11 @@ impl DefaultStringReporter { /// One cause is derived (already explained so one-line), /// the other is a one-line external cause, /// and finally we conclude with the current incompatibility. - fn explain_ref_and_external( + fn explain_ref_and_external( ref_id: usize, - derived: &Derived, - external: &External, - current_terms: &Map>, + derived: &Derived, + external: &External, + current_terms: &Map>, ) -> String { // TODO: order should be chosen to make it more logical. format!( @@ -394,9 +390,9 @@ impl DefaultStringReporter { } /// Add an external cause to the chain of explanations. - fn and_explain_external( - external: &External, - current_terms: &Map>, + fn and_explain_external( + external: &External, + current_terms: &Map>, ) -> String { format!( "And because {}, {}.", @@ -406,10 +402,10 @@ impl DefaultStringReporter { } /// Add an already explained incompat to the chain of explanations. - fn and_explain_ref( + fn and_explain_ref( ref_id: usize, - derived: &Derived, - current_terms: &Map>, + derived: &Derived, + current_terms: &Map>, ) -> String { format!( "And because {} ({}), {}.", @@ -420,10 +416,10 @@ impl DefaultStringReporter { } /// Add an already explained incompat to the chain of explanations. - fn and_explain_prior_and_external( - prior_external: &External, - external: &External, - current_terms: &Map>, + fn and_explain_prior_and_external( + prior_external: &External, + external: &External, + current_terms: &Map>, ) -> String { format!( "And because {} and {}, {}.", @@ -434,7 +430,7 @@ impl DefaultStringReporter { } /// Try to print terms of an incompatibility in a human-readable way. - pub fn string_terms(terms: &Map>) -> String { + pub fn string_terms(terms: &Map>) -> String { let terms_vec: Vec<_> = terms.iter().collect(); match terms_vec.as_slice() { [] => "version solving failed".into(), @@ -469,10 +465,10 @@ impl DefaultStringReporter { } } -impl Reporter for DefaultStringReporter { +impl Reporter for DefaultStringReporter { type Output = String; - fn report(derivation_tree: &DerivationTree) -> Self::Output { + fn report(derivation_tree: &DerivationTree) -> Self::Output { match derivation_tree { DerivationTree::External(external) => external.to_string(), DerivationTree::Derived(derived) => { diff --git a/src/report_bis.rs b/src/report_bis.rs deleted file mode 100644 index b6cecf49..00000000 --- a/src/report_bis.rs +++ /dev/null @@ -1,481 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! Build a report as clear as possible as to why -//! dependency solving failed. - -use std::fmt; -use std::ops::{Deref, DerefMut}; - -use crate::package::Package; -use crate::term_bis::Term; -use crate::type_aliases::Map; -use crate::version_set::VersionSet; - -/// Reporter trait. -pub trait Reporter { - /// Output type of the report. - type Output; - - /// Generate a report from the derivation tree - /// describing the resolution failure. - fn report(derivation_tree: &DerivationTree) -> Self::Output; -} - -/// Derivation tree resulting in the impossibility -/// to solve the dependencies of our root package. -#[derive(Debug, Clone)] -pub enum DerivationTree { - /// External incompatibility. - External(External), - /// Incompatibility derived from two others. - Derived(Derived), -} - -/// Incompatibilities that are not derived from others, -/// they have their own reason. -#[derive(Debug, Clone)] -pub enum External { - /// Initial incompatibility aiming at picking the root package for the first decision. - NotRoot(P, VS::V), - /// There are no versions in the given set for this package. - NoVersions(P, VS), - /// Dependencies of the package are unavailable for versions in that set. - UnavailableDependencies(P, VS), - /// Incompatibility coming from the dependencies of a given package. - FromDependencyOf(P, VS, P, VS), -} - -/// Incompatibility derived from two others. -#[derive(Debug, Clone)] -pub struct Derived { - /// Terms of the incompatibility. - pub terms: Map>, - /// Indicate if that incompatibility is present multiple times - /// in the derivation tree. - /// If that is the case, it has a unique id, provided in that option. - /// Then, we may want to only explain it once, - /// and refer to the explanation for the other times. - pub shared_id: Option, - /// First cause. - pub cause1: Box>, - /// Second cause. - pub cause2: Box>, -} - -impl DerivationTree { - /// Merge the [NoVersions](External::NoVersions) external incompatibilities - /// with the other one they are matched with - /// in a derived incompatibility. - /// This cleans up quite nicely the generated report. - /// You might want to do this if you know that the - /// [DependencyProvider](crate::solver::DependencyProvider) - /// was not run in some kind of offline mode that may not - /// have access to all versions existing. - pub fn collapse_no_versions(&mut self) { - match self { - DerivationTree::External(_) => {} - DerivationTree::Derived(derived) => { - match (derived.cause1.deref_mut(), derived.cause2.deref_mut()) { - (DerivationTree::External(External::NoVersions(p, r)), ref mut cause2) => { - cause2.collapse_no_versions(); - *self = cause2 - .clone() - .merge_no_versions(p.to_owned(), r.to_owned()) - .unwrap_or_else(|| self.to_owned()); - } - (ref mut cause1, DerivationTree::External(External::NoVersions(p, r))) => { - cause1.collapse_no_versions(); - *self = cause1 - .clone() - .merge_no_versions(p.to_owned(), r.to_owned()) - .unwrap_or_else(|| self.to_owned()); - } - _ => { - derived.cause1.collapse_no_versions(); - derived.cause2.collapse_no_versions(); - } - } - } - } - } - - fn merge_no_versions(self, package: P, set: VS) -> Option { - match self { - // TODO: take care of the Derived case. - // Once done, we can remove the Option. - DerivationTree::Derived(_) => Some(self), - DerivationTree::External(External::NotRoot(_, _)) => { - panic!("How did we end up with a NoVersions merged with a NotRoot?") - } - DerivationTree::External(External::NoVersions(_, r)) => Some(DerivationTree::External( - External::NoVersions(package, set.union(&r)), - )), - DerivationTree::External(External::UnavailableDependencies(_, r)) => Some( - DerivationTree::External(External::UnavailableDependencies(package, set.union(&r))), - ), - DerivationTree::External(External::FromDependencyOf(p1, r1, p2, r2)) => { - if p1 == package { - Some(DerivationTree::External(External::FromDependencyOf( - p1, - r1.union(&set), - p2, - r2, - ))) - } else { - Some(DerivationTree::External(External::FromDependencyOf( - p1, - r1, - p2, - r2.union(&set), - ))) - } - } - } - } -} - -impl fmt::Display for External { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::NotRoot(package, version) => { - write!(f, "we are solving dependencies of {} {}", package, version) - } - Self::NoVersions(package, set) => { - if set == &VS::full() { - write!(f, "there is no available version for {}", package) - } else { - write!(f, "there is no version of {} in {}", package, set) - } - } - Self::UnavailableDependencies(package, set) => { - if set == &VS::full() { - write!(f, "dependencies of {} are unavailable", package) - } else { - write!( - f, - "dependencies of {} at version {} are unavailable", - package, set - ) - } - } - Self::FromDependencyOf(p, set_p, dep, set_dep) => { - if set_p == &VS::full() && set_dep == &VS::full() { - write!(f, "{} depends on {}", p, dep) - } else if set_p == &VS::full() { - write!(f, "{} depends on {} {}", p, dep, set_dep) - } else if set_dep == &VS::full() { - write!(f, "{} {} depends on {}", p, set_p, dep) - } else { - write!(f, "{} {} depends on {} {}", p, set_p, dep, set_dep) - } - } - } - } -} - -/// Default reporter able to generate an explanation as a [String]. -pub struct DefaultStringReporter { - /// Number of explanations already with a line reference. - ref_count: usize, - /// Shared nodes that have already been marked with a line reference. - /// The incompatibility ids are the keys, and the line references are the values. - shared_with_ref: Map, - /// Accumulated lines of the report already generated. - lines: Vec, -} - -impl DefaultStringReporter { - /// Initialize the reporter. - fn new() -> Self { - Self { - ref_count: 0, - shared_with_ref: Map::default(), - lines: Vec::new(), - } - } - - fn build_recursive(&mut self, derived: &Derived) { - self.build_recursive_helper(derived); - if let Some(id) = derived.shared_id { - if self.shared_with_ref.get(&id) == None { - self.add_line_ref(); - self.shared_with_ref.insert(id, self.ref_count); - } - }; - } - - fn build_recursive_helper(&mut self, current: &Derived) { - match (current.cause1.deref(), current.cause2.deref()) { - (DerivationTree::External(external1), DerivationTree::External(external2)) => { - // Simplest case, we just combine two external incompatibilities. - self.lines.push(Self::explain_both_external( - external1, - external2, - ¤t.terms, - )); - } - (DerivationTree::Derived(derived), DerivationTree::External(external)) => { - // One cause is derived, so we explain this first - // then we add the one-line external part - // and finally conclude with the current incompatibility. - self.report_one_each(derived, external, ¤t.terms); - } - (DerivationTree::External(external), DerivationTree::Derived(derived)) => { - self.report_one_each(derived, external, ¤t.terms); - } - (DerivationTree::Derived(derived1), DerivationTree::Derived(derived2)) => { - // This is the most complex case since both causes are also derived. - match ( - self.line_ref_of(derived1.shared_id), - self.line_ref_of(derived2.shared_id), - ) { - // If both causes already have been referenced (shared_id), - // the explanation simply uses those references. - (Some(ref1), Some(ref2)) => self.lines.push(Self::explain_both_ref( - ref1, - derived1, - ref2, - derived2, - ¤t.terms, - )), - // Otherwise, if one only has a line number reference, - // we recursively call the one without reference and then - // add the one with reference to conclude. - (Some(ref1), None) => { - self.build_recursive(derived2); - self.lines - .push(Self::and_explain_ref(ref1, derived1, ¤t.terms)); - } - (None, Some(ref2)) => { - self.build_recursive(derived1); - self.lines - .push(Self::and_explain_ref(ref2, derived2, ¤t.terms)); - } - // Finally, if no line reference exists yet, - // we call recursively the first one and then, - // - if this was a shared node, it will get a line ref - // and we can simply recall this with the current node. - // - otherwise, we add a line reference to it, - // recursively call on the second node, - // and finally conclude. - (None, None) => { - self.build_recursive(derived1); - if derived1.shared_id != None { - self.lines.push("".into()); - self.build_recursive(current); - } else { - self.add_line_ref(); - let ref1 = self.ref_count; - self.lines.push("".into()); - self.build_recursive(derived2); - self.lines - .push(Self::and_explain_ref(ref1, derived1, ¤t.terms)); - } - } - } - } - } - } - - /// Report a derived and an external incompatibility. - /// - /// The result will depend on the fact that the derived incompatibility - /// has already been explained or not. - fn report_one_each( - &mut self, - derived: &Derived, - external: &External, - current_terms: &Map>, - ) { - match self.line_ref_of(derived.shared_id) { - Some(ref_id) => self.lines.push(Self::explain_ref_and_external( - ref_id, - derived, - external, - current_terms, - )), - None => self.report_recurse_one_each(derived, external, current_terms), - } - } - - /// Report one derived (without a line ref yet) and one external. - fn report_recurse_one_each( - &mut self, - derived: &Derived, - external: &External, - current_terms: &Map>, - ) { - match (derived.cause1.deref(), derived.cause2.deref()) { - // If the derived cause has itself one external prior cause, - // we can chain the external explanations. - (DerivationTree::Derived(prior_derived), DerivationTree::External(prior_external)) => { - self.build_recursive(prior_derived); - self.lines.push(Self::and_explain_prior_and_external( - prior_external, - external, - current_terms, - )); - } - // If the derived cause has itself one external prior cause, - // we can chain the external explanations. - (DerivationTree::External(prior_external), DerivationTree::Derived(prior_derived)) => { - self.build_recursive(prior_derived); - self.lines.push(Self::and_explain_prior_and_external( - prior_external, - external, - current_terms, - )); - } - _ => { - self.build_recursive(derived); - self.lines - .push(Self::and_explain_external(external, current_terms)); - } - } - } - - // String explanations ##################################################### - - /// Simplest case, we just combine two external incompatibilities. - fn explain_both_external( - external1: &External, - external2: &External, - current_terms: &Map>, - ) -> String { - // TODO: order should be chosen to make it more logical. - format!( - "Because {} and {}, {}.", - external1, - external2, - Self::string_terms(current_terms) - ) - } - - /// Both causes have already been explained so we use their refs. - fn explain_both_ref( - ref_id1: usize, - derived1: &Derived, - ref_id2: usize, - derived2: &Derived, - current_terms: &Map>, - ) -> String { - // TODO: order should be chosen to make it more logical. - format!( - "Because {} ({}) and {} ({}), {}.", - Self::string_terms(&derived1.terms), - ref_id1, - Self::string_terms(&derived2.terms), - ref_id2, - Self::string_terms(current_terms) - ) - } - - /// One cause is derived (already explained so one-line), - /// the other is a one-line external cause, - /// and finally we conclude with the current incompatibility. - fn explain_ref_and_external( - ref_id: usize, - derived: &Derived, - external: &External, - current_terms: &Map>, - ) -> String { - // TODO: order should be chosen to make it more logical. - format!( - "Because {} ({}) and {}, {}.", - Self::string_terms(&derived.terms), - ref_id, - external, - Self::string_terms(current_terms) - ) - } - - /// Add an external cause to the chain of explanations. - fn and_explain_external( - external: &External, - current_terms: &Map>, - ) -> String { - format!( - "And because {}, {}.", - external, - Self::string_terms(current_terms) - ) - } - - /// Add an already explained incompat to the chain of explanations. - fn and_explain_ref( - ref_id: usize, - derived: &Derived, - current_terms: &Map>, - ) -> String { - format!( - "And because {} ({}), {}.", - Self::string_terms(&derived.terms), - ref_id, - Self::string_terms(current_terms) - ) - } - - /// Add an already explained incompat to the chain of explanations. - fn and_explain_prior_and_external( - prior_external: &External, - external: &External, - current_terms: &Map>, - ) -> String { - format!( - "And because {} and {}, {}.", - prior_external, - external, - Self::string_terms(current_terms) - ) - } - - /// Try to print terms of an incompatibility in a human-readable way. - pub fn string_terms(terms: &Map>) -> String { - let terms_vec: Vec<_> = terms.iter().collect(); - match terms_vec.as_slice() { - [] => "version solving failed".into(), - // TODO: special case when that unique package is root. - [(package, Term::Positive(range))] => format!("{} {} is forbidden", package, range), - [(package, Term::Negative(range))] => format!("{} {} is mandatory", package, range), - [(p1, Term::Positive(r1)), (p2, Term::Negative(r2))] => { - External::FromDependencyOf(p1, r1.clone(), p2, r2.clone()).to_string() - } - [(p1, Term::Negative(r1)), (p2, Term::Positive(r2))] => { - External::FromDependencyOf(p2, r2.clone(), p1, r1.clone()).to_string() - } - slice => { - let str_terms: Vec<_> = slice.iter().map(|(p, t)| format!("{} {}", p, t)).collect(); - str_terms.join(", ") + " are incompatible" - } - } - } - - // Helper functions ######################################################## - - fn add_line_ref(&mut self) { - let new_count = self.ref_count + 1; - self.ref_count = new_count; - if let Some(line) = self.lines.last_mut() { - *line = format!("{} ({})", line, new_count); - } - } - - fn line_ref_of(&self, shared_id: Option) -> Option { - shared_id.and_then(|id| self.shared_with_ref.get(&id).cloned()) - } -} - -impl Reporter for DefaultStringReporter { - type Output = String; - - fn report(derivation_tree: &DerivationTree) -> Self::Output { - match derivation_tree { - DerivationTree::External(external) => external.to_string(), - DerivationTree::Derived(derived) => { - let mut reporter = Self::new(); - reporter.build_recursive(derived); - reporter.lines.join("\n") - } - } - } -} diff --git a/src/solver.rs b/src/solver.rs index 2de5f3f3..4077aac4 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -73,19 +73,18 @@ use crate::error::PubGrubError; use crate::internal::core::State; use crate::internal::incompatibility::Incompatibility; use crate::package::Package; -use crate::range::Range; use crate::type_aliases::{DependencyConstraints, Map, SelectedDependencies}; -use crate::version::Version; +use crate::version_set::VersionSet; /// Main function of the library. /// Finds a set of packages satisfying dependency bounds for a given package + version pair. -pub fn resolve( - dependency_provider: &impl DependencyProvider, +pub fn resolve( + dependency_provider: &impl DependencyProvider, package: P, - version: impl Into, -) -> Result, PubGrubError> { + version: impl Into, +) -> Result, PubGrubError> { let mut state = State::init(package.clone(), version.into()); - let mut added_dependencies: Map> = Map::default(); + let mut added_dependencies: Map> = Map::default(); let mut next = package; loop { dependency_provider @@ -159,7 +158,7 @@ pub fn resolve( version: v.clone(), }); } - if let Some((dependent, _)) = x.iter().find(|(_, r)| r == &&Range::none()) { + if let Some((dependent, _)) = x.iter().find(|(_, r)| r == &&VS::empty()) { return Err(PubGrubError::DependencyOnTheEmptySet { package: p.clone(), version: v.clone(), @@ -203,16 +202,16 @@ pub fn resolve( /// An enum used by [DependencyProvider] that holds information about package dependencies. /// For each [Package] there is a [Range] of concrete versions it allows as a dependency. #[derive(Clone)] -pub enum Dependencies { +pub enum Dependencies { /// Package dependencies are unavailable. Unknown, /// Container for all available package versions. - Known(DependencyConstraints), + Known(DependencyConstraints), } /// Trait that allows the algorithm to retrieve available packages and their dependencies. /// An implementor needs to be supplied to the [resolve] function. -pub trait DependencyProvider { +pub trait DependencyProvider { /// [Decision making](https://github.com/dart-lang/pub/blob/master/doc/solver.md#decision-making) /// is the process of choosing the next package /// and version that will be appended to the partial solution. @@ -238,18 +237,18 @@ pub trait DependencyProvider { /// of the available versions in preference order for any package. /// /// Note: the type `T` ensures that this returns an item from the `packages` argument. - fn choose_package_version, U: Borrow>>( + fn choose_package_version, U: Borrow>( &self, potential_packages: impl Iterator, - ) -> Result<(T, Option), Box>; + ) -> Result<(T, Option), Box>; /// Retrieves the package dependencies. /// Return [Dependencies::Unknown] if its dependencies are unknown. fn get_dependencies( &self, package: &P, - version: &V, - ) -> Result, Box>; + version: &VS::V, + ) -> Result, Box>; /// This is called fairly regularly during the resolution, /// if it returns an Err then resolution will be terminated. @@ -268,26 +267,25 @@ pub trait DependencyProvider { /// The helper finds the package from the `packages` argument with the fewest versions from /// `list_available_versions` contained in the constraints. Then takes that package and finds the /// first version contained in the constraints. -pub fn choose_package_with_fewest_versions( +pub fn choose_package_with_fewest_versions( list_available_versions: F, potential_packages: impl Iterator, -) -> (T, Option) +) -> (T, Option) where T: Borrow

, - U: Borrow>, - I: Iterator, + U: Borrow, + I: Iterator, F: Fn(&P) -> I, { - let count_valid = |(p, range): &(T, U)| { + let count_valid = |(p, set): &(T, U)| { list_available_versions(p.borrow()) - .filter(|v| range.borrow().contains(v.borrow())) + .filter(|v| set.borrow().contains(v.borrow())) .count() }; - let (pkg, range) = potential_packages + let (pkg, set) = potential_packages .min_by_key(count_valid) .expect("potential_packages gave us an empty iterator"); - let version = - list_available_versions(pkg.borrow()).find(|v| range.borrow().contains(v.borrow())); + let version = list_available_versions(pkg.borrow()).find(|v| set.borrow().contains(v.borrow())); (pkg, version) } @@ -295,11 +293,11 @@ where #[derive(Debug, Clone, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", serde(transparent))] -pub struct OfflineDependencyProvider { - dependencies: Map>>, +pub struct OfflineDependencyProvider { + dependencies: Map>>, } -impl OfflineDependencyProvider { +impl OfflineDependencyProvider { /// Creates an empty OfflineDependencyProvider with no dependencies. pub fn new() -> Self { Self { @@ -317,10 +315,10 @@ impl OfflineDependencyProvider { /// The API does not allow to add dependencies one at a time to uphold an assumption that /// [OfflineDependencyProvider.get_dependencies(p, v)](OfflineDependencyProvider::get_dependencies) /// provides all dependencies of a given package (p) and version (v) pair. - pub fn add_dependencies)>>( + pub fn add_dependencies>( &mut self, package: P, - version: impl Into, + version: impl Into, dependencies: I, ) { let package_deps = dependencies.into_iter().collect(); @@ -340,13 +338,13 @@ impl OfflineDependencyProvider { /// Lists versions of saved packages in sorted order. /// Returns [None] if no information is available regarding that package. - pub fn versions(&self, package: &P) -> Option> { + pub fn versions(&self, package: &P) -> Option> { self.dependencies.get(package).map(|k| k.keys()) } /// Lists dependencies of a given package and version. /// Returns [None] if no information is available regarding that package and version pair. - fn dependencies(&self, package: &P, version: &V) -> Option> { + fn dependencies(&self, package: &P, version: &VS::V) -> Option> { self.dependencies.get(package)?.get(version).cloned() } } @@ -355,11 +353,11 @@ impl OfflineDependencyProvider { /// contains all dependency information available in memory. /// Packages are picked with the fewest versions contained in the constraints first. /// Versions are picked with the newest versions first. -impl DependencyProvider for OfflineDependencyProvider { - fn choose_package_version, U: Borrow>>( +impl DependencyProvider for OfflineDependencyProvider { + fn choose_package_version, U: Borrow>( &self, potential_packages: impl Iterator, - ) -> Result<(T, Option), Box> { + ) -> Result<(T, Option), Box> { Ok(choose_package_with_fewest_versions( |p| { self.dependencies @@ -376,8 +374,8 @@ impl DependencyProvider for OfflineDependencyProvi fn get_dependencies( &self, package: &P, - version: &V, - ) -> Result, Box> { + version: &VS::V, + ) -> Result, Box> { Ok(match self.dependencies(package, version) { None => Dependencies::Unknown, Some(dependencies) => Dependencies::Known(dependencies), diff --git a/src/solver_bis.rs b/src/solver_bis.rs deleted file mode 100644 index 63fc6848..00000000 --- a/src/solver_bis.rs +++ /dev/null @@ -1,384 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! PubGrub version solving algorithm. -//! -//! It consists in efficiently finding a set of packages and versions -//! that satisfy all the constraints of a given project dependencies. -//! In addition, when that is not possible, -//! PubGrub tries to provide a very human-readable and clear -//! explanation as to why that failed. -//! Below is an example of explanation present in -//! the introductory blog post about PubGrub -//! -//! ```txt -//! Because dropdown >=2.0.0 depends on icons >=2.0.0 and -//! root depends on icons <2.0.0, dropdown >=2.0.0 is forbidden. -//! -//! And because menu >=1.1.0 depends on dropdown >=2.0.0, -//! menu >=1.1.0 is forbidden. -//! -//! And because menu <1.1.0 depends on dropdown >=1.0.0 <2.0.0 -//! which depends on intl <4.0.0, every version of menu -//! requires intl <4.0.0. -//! -//! So, because root depends on both menu >=1.0.0 and intl >=5.0.0, -//! version solving failed. -//! ``` -//! -//! The algorithm is generic and works for any type of dependency system -//! as long as packages (P) and versions (V) implement -//! the [Package](crate::package::Package) and [Version](crate::version::Version) traits. -//! [Package](crate::package::Package) is strictly equivalent and automatically generated -//! for any type that implement [Clone] + [Eq] + [Hash] + [Debug] + [Display](std::fmt::Display). -//! [Version](crate::version::Version) simply states that versions are ordered, -//! that there should be -//! a minimal [lowest](crate::version::Version::lowest) version (like 0.0.0 in semantic versions), -//! and that for any version, it is possible to compute -//! what the next version closest to this one is ([bump](crate::version::Version::bump)). -//! For semantic versions, [bump](crate::version::Version::bump) corresponds to -//! an increment of the patch number. -//! -//! ## API -//! -//! ``` -//! # use pubgrub::solver::{resolve, OfflineDependencyProvider}; -//! # use pubgrub::version::NumberVersion; -//! # use pubgrub::error::PubGrubError; -//! # -//! # fn try_main() -> Result<(), PubGrubError<&'static str, NumberVersion>> { -//! # let dependency_provider = OfflineDependencyProvider::<&str, NumberVersion>::new(); -//! # let package = "root"; -//! # let version = 1; -//! let solution = resolve(&dependency_provider, package, version)?; -//! # Ok(()) -//! # } -//! # fn main() { -//! # assert!(matches!(try_main(), Err(PubGrubError::NoSolution(_)))); -//! # } -//! ``` -//! -//! Where `dependency_provider` supplies the list of available packages and versions, -//! as well as the dependencies of every available package -//! by implementing the [DependencyProvider] trait. -//! The call to [resolve] for a given package at a given version -//! will compute the set of packages and versions needed -//! to satisfy the dependencies of that package and version pair. -//! If there is no solution, the reason will be provided as clear as possible. - -use std::borrow::Borrow; -use std::collections::{BTreeMap, BTreeSet as Set}; -use std::error::Error; - -use crate::error_bis::PubGrubError; -use crate::internal::core_bis::State; -use crate::internal::incompatibility_bis::Incompatibility; -use crate::package::Package; -use crate::type_aliases_bis::{DependencyConstraints, Map, SelectedDependencies}; -use crate::version_set::VersionSet; - -/// Main function of the library. -/// Finds a set of packages satisfying dependency bounds for a given package + version pair. -pub fn resolve( - dependency_provider: &impl DependencyProvider, - package: P, - version: impl Into, -) -> Result, PubGrubError> { - let mut state = State::init(package.clone(), version.into()); - let mut added_dependencies: Map> = Map::default(); - let mut next = package; - loop { - dependency_provider - .should_cancel() - .map_err(|err| PubGrubError::ErrorInShouldCancel(err))?; - - state.unit_propagation(next)?; - - let potential_packages = state.partial_solution.potential_packages(); - if potential_packages.is_none() { - drop(potential_packages); - // The borrow checker did not like using a match on potential_packages. - // This `if ... is_none ... drop` is a workaround. - // I believe this is a case where Polonius could help, when and if it lands in rustc. - return state.partial_solution.extract_solution().ok_or_else(|| { - PubGrubError::Failure( - "How did we end up with no package to choose but no solution?".into(), - ) - }); - } - let decision = dependency_provider - .choose_package_version(potential_packages.unwrap()) - .map_err(PubGrubError::ErrorChoosingPackageVersion)?; - next = decision.0.clone(); - - // Pick the next compatible version. - let term_intersection = state - .partial_solution - .term_intersection_for_package(&next) - .expect("a package was chosen but we don't have a term."); - let v = match decision.1 { - None => { - let inc = Incompatibility::no_versions(next.clone(), term_intersection.clone()); - state.add_incompatibility(inc); - continue; - } - Some(x) => x, - }; - if !term_intersection.contains(&v) { - return Err(PubGrubError::ErrorChoosingPackageVersion( - "choose_package_version picked an incompatible version".into(), - )); - } - - if added_dependencies - .entry(next.clone()) - .or_default() - .insert(v.clone()) - { - // Retrieve that package dependencies. - let p = &next; - let dependencies = - match dependency_provider - .get_dependencies(&p, &v) - .map_err(|err| PubGrubError::ErrorRetrievingDependencies { - package: p.clone(), - version: v.clone(), - source: err, - })? { - Dependencies::Unknown => { - state.add_incompatibility(Incompatibility::unavailable_dependencies( - p.clone(), - v.clone(), - )); - continue; - } - Dependencies::Known(x) => { - if x.contains_key(&p) { - return Err(PubGrubError::SelfDependency { - package: p.clone(), - version: v.clone(), - }); - } - if let Some((dependent, _)) = x.iter().find(|(_, r)| r == &&VS::empty()) { - return Err(PubGrubError::DependencyOnTheEmptySet { - package: p.clone(), - version: v.clone(), - dependent: dependent.clone(), - }); - } - x - } - }; - - // Add that package and version if the dependencies are not problematic. - let dep_incompats = - state.add_incompatibility_from_dependencies(p.clone(), v.clone(), &dependencies); - - // TODO: I don't think this check can actually happen. - // We might want to put it under #[cfg(debug_assertions)]. - if state.incompatibility_store[dep_incompats.clone()] - .iter() - .any(|incompat| state.is_terminal(incompat)) - { - // For a dependency incompatibility to be terminal, - // it can only mean that root depend on not root? - return Err(PubGrubError::Failure( - "Root package depends on itself at a different version?".into(), - )); - } - state.partial_solution.add_version( - p.clone(), - v, - dep_incompats, - &state.incompatibility_store, - ); - } else { - // `dep_incompats` are already in `incompatibilities` so we know there are not satisfied - // terms and can add the decision directly. - state.partial_solution.add_decision(next.clone(), v); - } - } -} - -/// An enum used by [DependencyProvider] that holds information about package dependencies. -/// For each [Package] there is a [Range] of concrete versions it allows as a dependency. -#[derive(Clone)] -pub enum Dependencies { - /// Package dependencies are unavailable. - Unknown, - /// Container for all available package versions. - Known(DependencyConstraints), -} - -/// Trait that allows the algorithm to retrieve available packages and their dependencies. -/// An implementor needs to be supplied to the [resolve] function. -pub trait DependencyProvider { - /// [Decision making](https://github.com/dart-lang/pub/blob/master/doc/solver.md#decision-making) - /// is the process of choosing the next package - /// and version that will be appended to the partial solution. - /// Every time such a decision must be made, - /// potential valid packages and version ranges are preselected by the resolver, - /// and the dependency provider must choose. - /// - /// The strategy employed to choose such package and version - /// cannot change the existence of a solution or not, - /// but can drastically change the performances of the solver, - /// or the properties of the solution. - /// The documentation of Pub (PubGrub implementation for the dart programming language) - /// states the following: - /// - /// > Pub chooses the latest matching version of the package - /// > with the fewest versions that match the outstanding constraint. - /// > This tends to find conflicts earlier if any exist, - /// > since these packages will run out of versions to try more quickly. - /// > But there's likely room for improvement in these heuristics. - /// - /// A helper function [choose_package_with_fewest_versions] is provided to ease - /// implementations of this method if you can produce an iterator - /// of the available versions in preference order for any package. - /// - /// Note: the type `T` ensures that this returns an item from the `packages` argument. - fn choose_package_version, U: Borrow>( - &self, - potential_packages: impl Iterator, - ) -> Result<(T, Option), Box>; - - /// Retrieves the package dependencies. - /// Return [Dependencies::Unknown] if its dependencies are unknown. - fn get_dependencies( - &self, - package: &P, - version: &VS::V, - ) -> Result, Box>; - - /// This is called fairly regularly during the resolution, - /// if it returns an Err then resolution will be terminated. - /// This is helpful if you want to add some form of early termination like a timeout, - /// or you want to add some form of user feedback if things are taking a while. - /// If not provided the resolver will run as long as needed. - fn should_cancel(&self) -> Result<(), Box> { - Ok(()) - } -} - -/// This is a helper function to make it easy to implement -/// [DependencyProvider::choose_package_version]. -/// It takes a function `list_available_versions` that takes a package and returns an iterator -/// of the available versions in preference order. -/// The helper finds the package from the `packages` argument with the fewest versions from -/// `list_available_versions` contained in the constraints. Then takes that package and finds the -/// first version contained in the constraints. -pub fn choose_package_with_fewest_versions( - list_available_versions: F, - potential_packages: impl Iterator, -) -> (T, Option) -where - T: Borrow

, - U: Borrow, - I: Iterator, - F: Fn(&P) -> I, -{ - let count_valid = |(p, set): &(T, U)| { - list_available_versions(p.borrow()) - .filter(|v| set.borrow().contains(v.borrow())) - .count() - }; - let (pkg, set) = potential_packages - .min_by_key(count_valid) - .expect("potential_packages gave us an empty iterator"); - let version = list_available_versions(pkg.borrow()).find(|v| set.borrow().contains(v.borrow())); - (pkg, version) -} - -/// A basic implementation of [DependencyProvider]. -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(transparent))] -pub struct OfflineDependencyProvider { - dependencies: Map>>, -} - -impl OfflineDependencyProvider { - /// Creates an empty OfflineDependencyProvider with no dependencies. - pub fn new() -> Self { - Self { - dependencies: Map::default(), - } - } - - /// Registers the dependencies of a package and version pair. - /// Dependencies must be added with a single call to - /// [add_dependencies](OfflineDependencyProvider::add_dependencies). - /// All subsequent calls to - /// [add_dependencies](OfflineDependencyProvider::add_dependencies) for a given - /// package version pair will replace the dependencies by the new ones. - /// - /// The API does not allow to add dependencies one at a time to uphold an assumption that - /// [OfflineDependencyProvider.get_dependencies(p, v)](OfflineDependencyProvider::get_dependencies) - /// provides all dependencies of a given package (p) and version (v) pair. - pub fn add_dependencies>( - &mut self, - package: P, - version: impl Into, - dependencies: I, - ) { - let package_deps = dependencies.into_iter().collect(); - let v = version.into(); - *self - .dependencies - .entry(package) - .or_default() - .entry(v) - .or_default() = package_deps; - } - - /// Lists packages that have been saved. - pub fn packages(&self) -> impl Iterator { - self.dependencies.keys() - } - - /// Lists versions of saved packages in sorted order. - /// Returns [None] if no information is available regarding that package. - pub fn versions(&self, package: &P) -> Option> { - self.dependencies.get(package).map(|k| k.keys()) - } - - /// Lists dependencies of a given package and version. - /// Returns [None] if no information is available regarding that package and version pair. - fn dependencies(&self, package: &P, version: &VS::V) -> Option> { - self.dependencies.get(package)?.get(version).cloned() - } -} - -/// An implementation of [DependencyProvider] that -/// contains all dependency information available in memory. -/// Packages are picked with the fewest versions contained in the constraints first. -/// Versions are picked with the newest versions first. -impl DependencyProvider for OfflineDependencyProvider { - fn choose_package_version, U: Borrow>( - &self, - potential_packages: impl Iterator, - ) -> Result<(T, Option), Box> { - Ok(choose_package_with_fewest_versions( - |p| { - self.dependencies - .get(p) - .into_iter() - .flat_map(|k| k.keys()) - .rev() - .cloned() - }, - potential_packages, - )) - } - - fn get_dependencies( - &self, - package: &P, - version: &VS::V, - ) -> Result, Box> { - Ok(match self.dependencies(package, version) { - None => Dependencies::Unknown, - Some(dependencies) => Dependencies::Known(dependencies), - }) - } -} diff --git a/src/term.rs b/src/term.rs index bc038acf..a2f4f90c 100644 --- a/src/term.rs +++ b/src/term.rs @@ -3,38 +3,37 @@ //! A term is the fundamental unit of operation of the PubGrub algorithm. //! It is a positive or negative expression regarding a set of versions. -use crate::range::Range; -use crate::version::Version; -use std::fmt; +use crate::version_set::VersionSet; +use std::fmt::{self, Display}; /// A positive or negative expression regarding a set of versions. #[derive(Debug, Clone, Eq, PartialEq)] -pub enum Term { +pub enum Term { /// For example, "1.0.0 <= v < 2.0.0" is a positive expression /// that is evaluated true if a version is selected /// and comprised between version 1.0.0 and version 2.0.0. - Positive(Range), + Positive(VS), /// The term "not v < 3.0.0" is a negative expression /// that is evaluated true if a version is selected >= 3.0.0 /// or if no version is selected at all. - Negative(Range), + Negative(VS), } /// Base methods. -impl Term { +impl Term { /// A term that is always true. pub(crate) fn any() -> Self { - Self::Negative(Range::none()) + Self::Negative(VS::empty()) } /// A term that is never true. pub(crate) fn empty() -> Self { - Self::Positive(Range::none()) + Self::Positive(VS::empty()) } /// A positive term containing exactly that version. - pub(crate) fn exact(version: V) -> Self { - Self::Positive(Range::exact(version)) + pub(crate) fn exact(version: VS::V) -> Self { + Self::Positive(VS::singleton(version)) } /// Simply check if a term is positive. @@ -50,41 +49,41 @@ impl Term { /// the opposite of the evaluation of the original one. pub(crate) fn negate(&self) -> Self { match self { - Self::Positive(range) => Self::Negative(range.clone()), - Self::Negative(range) => Self::Positive(range.clone()), + Self::Positive(set) => Self::Negative(set.clone()), + Self::Negative(set) => Self::Positive(set.clone()), } } /// Evaluate a term regarding a given choice of version. - pub(crate) fn contains(&self, v: &V) -> bool { + pub(crate) fn contains(&self, v: &VS::V) -> bool { match self { - Self::Positive(range) => range.contains(v), - Self::Negative(range) => !(range.contains(v)), + Self::Positive(set) => set.contains(v), + Self::Negative(set) => !(set.contains(v)), } } - /// Unwrap the range contains in a positive term. - /// Will panic if used on a negative range. - pub(crate) fn unwrap_positive(&self) -> &Range { + /// Unwrap the set contained in a positive term. + /// Will panic if used on a negative set. + pub(crate) fn unwrap_positive(&self) -> &VS { match self { - Self::Positive(range) => range, - _ => panic!("Negative term cannot unwrap positive range"), + Self::Positive(set) => set, + _ => panic!("Negative term cannot unwrap positive set"), } } } /// Set operations with terms. -impl Term { +impl Term { /// Compute the intersection of two terms. /// If at least one term is positive, the intersection is also positive. - pub(crate) fn intersection(&self, other: &Term) -> Term { + pub(crate) fn intersection(&self, other: &Self) -> Self { match (self, other) { (Self::Positive(r1), Self::Positive(r2)) => Self::Positive(r1.intersection(r2)), (Self::Positive(r1), Self::Negative(r2)) => { - Self::Positive(r1.intersection(&r2.negate())) + Self::Positive(r1.intersection(&r2.complement())) } (Self::Negative(r1), Self::Positive(r2)) => { - Self::Positive(r1.negate().intersection(r2)) + Self::Positive(r1.complement().intersection(r2)) } (Self::Negative(r1), Self::Negative(r2)) => Self::Negative(r1.union(r2)), } @@ -92,14 +91,14 @@ impl Term { /// Compute the union of two terms. /// If at least one term is negative, the union is also negative. - pub(crate) fn union(&self, other: &Term) -> Term { + pub(crate) fn union(&self, other: &Self) -> Self { (self.negate().intersection(&other.negate())).negate() } /// Indicate if this term is a subset of another term. /// Just like for sets, we say that t1 is a subset of t2 /// if and only if t1 ∩ t2 = t1. - pub(crate) fn subset_of(&self, other: &Term) -> bool { + pub(crate) fn subset_of(&self, other: &Self) -> bool { self == &self.intersection(other) } } @@ -120,7 +119,7 @@ pub(crate) enum Relation { } /// Relation between terms. -impl<'a, V: 'a + Version> Term { +impl Term { /// Check if a set of terms satisfies this term. /// /// We say that a set of terms S "satisfies" a term t @@ -129,7 +128,7 @@ impl<'a, V: 'a + Version> Term { /// It turns out that this can also be expressed with set operations: /// S satisfies t if and only if ⋂ S ⊆ t #[cfg(test)] - fn satisfied_by(&self, terms_intersection: &Term) -> bool { + fn satisfied_by(&self, terms_intersection: &Self) -> bool { terms_intersection.subset_of(self) } @@ -142,13 +141,13 @@ impl<'a, V: 'a + Version> Term { /// S contradicts t if and only if ⋂ S is disjoint with t /// S contradicts t if and only if (⋂ S) ⋂ t = ∅ #[cfg(test)] - fn contradicted_by(&self, terms_intersection: &Term) -> bool { + fn contradicted_by(&self, terms_intersection: &Self) -> bool { terms_intersection.intersection(self) == Self::empty() } /// Check if a set of terms satisfies or contradicts a given term. /// Otherwise the relation is inconclusive. - pub(crate) fn relation_with(&self, other_terms_intersection: &Term) -> Relation { + pub(crate) fn relation_with(&self, other_terms_intersection: &Self) -> Relation { let full_intersection = self.intersection(other_terms_intersection); if &full_intersection == other_terms_intersection { Relation::Satisfied @@ -160,19 +159,20 @@ impl<'a, V: 'a + Version> Term { } } -impl AsRef> for Term { - fn as_ref(&self) -> &Term { - &self +// impl AsRef> for Term { +impl AsRef for Term { + fn as_ref(&self) -> &Self { + self } } // REPORT ###################################################################### -impl fmt::Display for Term { +impl Display for Term { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Positive(range) => write!(f, "{}", range), - Self::Negative(range) => write!(f, "Not ( {} )", range), + Self::Positive(set) => write!(f, "{}", set), + Self::Negative(set) => write!(f, "Not ( {} )", set), } } } @@ -182,10 +182,11 @@ impl fmt::Display for Term { #[cfg(test)] pub mod tests { use super::*; + use crate::range::Range; use crate::version::NumberVersion; use proptest::prelude::*; - pub fn strategy() -> impl Strategy> { + pub fn strategy() -> impl Strategy>> { prop_oneof![ crate::range::tests::strategy().prop_map(Term::Positive), crate::range::tests::strategy().prop_map(Term::Negative), diff --git a/src/term_bis.rs b/src/term_bis.rs deleted file mode 100644 index a2f4f90c..00000000 --- a/src/term_bis.rs +++ /dev/null @@ -1,213 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! A term is the fundamental unit of operation of the PubGrub algorithm. -//! It is a positive or negative expression regarding a set of versions. - -use crate::version_set::VersionSet; -use std::fmt::{self, Display}; - -/// A positive or negative expression regarding a set of versions. -#[derive(Debug, Clone, Eq, PartialEq)] -pub enum Term { - /// For example, "1.0.0 <= v < 2.0.0" is a positive expression - /// that is evaluated true if a version is selected - /// and comprised between version 1.0.0 and version 2.0.0. - Positive(VS), - /// The term "not v < 3.0.0" is a negative expression - /// that is evaluated true if a version is selected >= 3.0.0 - /// or if no version is selected at all. - Negative(VS), -} - -/// Base methods. -impl Term { - /// A term that is always true. - pub(crate) fn any() -> Self { - Self::Negative(VS::empty()) - } - - /// A term that is never true. - pub(crate) fn empty() -> Self { - Self::Positive(VS::empty()) - } - - /// A positive term containing exactly that version. - pub(crate) fn exact(version: VS::V) -> Self { - Self::Positive(VS::singleton(version)) - } - - /// Simply check if a term is positive. - pub(crate) fn is_positive(&self) -> bool { - match self { - Self::Positive(_) => true, - Self::Negative(_) => false, - } - } - - /// Negate a term. - /// Evaluation of a negated term always returns - /// the opposite of the evaluation of the original one. - pub(crate) fn negate(&self) -> Self { - match self { - Self::Positive(set) => Self::Negative(set.clone()), - Self::Negative(set) => Self::Positive(set.clone()), - } - } - - /// Evaluate a term regarding a given choice of version. - pub(crate) fn contains(&self, v: &VS::V) -> bool { - match self { - Self::Positive(set) => set.contains(v), - Self::Negative(set) => !(set.contains(v)), - } - } - - /// Unwrap the set contained in a positive term. - /// Will panic if used on a negative set. - pub(crate) fn unwrap_positive(&self) -> &VS { - match self { - Self::Positive(set) => set, - _ => panic!("Negative term cannot unwrap positive set"), - } - } -} - -/// Set operations with terms. -impl Term { - /// Compute the intersection of two terms. - /// If at least one term is positive, the intersection is also positive. - pub(crate) fn intersection(&self, other: &Self) -> Self { - match (self, other) { - (Self::Positive(r1), Self::Positive(r2)) => Self::Positive(r1.intersection(r2)), - (Self::Positive(r1), Self::Negative(r2)) => { - Self::Positive(r1.intersection(&r2.complement())) - } - (Self::Negative(r1), Self::Positive(r2)) => { - Self::Positive(r1.complement().intersection(r2)) - } - (Self::Negative(r1), Self::Negative(r2)) => Self::Negative(r1.union(r2)), - } - } - - /// Compute the union of two terms. - /// If at least one term is negative, the union is also negative. - pub(crate) fn union(&self, other: &Self) -> Self { - (self.negate().intersection(&other.negate())).negate() - } - - /// Indicate if this term is a subset of another term. - /// Just like for sets, we say that t1 is a subset of t2 - /// if and only if t1 ∩ t2 = t1. - pub(crate) fn subset_of(&self, other: &Self) -> bool { - self == &self.intersection(other) - } -} - -/// Describe a relation between a set of terms S and another term t. -/// -/// As a shorthand, we say that a term v -/// satisfies or contradicts a term t if {v} satisfies or contradicts it. -pub(crate) enum Relation { - /// We say that a set of terms S "satisfies" a term t - /// if t must be true whenever every term in S is true. - Satisfied, - /// Conversely, S "contradicts" t if t must be false - /// whenever every term in S is true. - Contradicted, - /// If neither of these is true we say that S is "inconclusive" for t. - Inconclusive, -} - -/// Relation between terms. -impl Term { - /// Check if a set of terms satisfies this term. - /// - /// We say that a set of terms S "satisfies" a term t - /// if t must be true whenever every term in S is true. - /// - /// It turns out that this can also be expressed with set operations: - /// S satisfies t if and only if ⋂ S ⊆ t - #[cfg(test)] - fn satisfied_by(&self, terms_intersection: &Self) -> bool { - terms_intersection.subset_of(self) - } - - /// Check if a set of terms contradicts this term. - /// - /// We say that a set of terms S "contradicts" a term t - /// if t must be false whenever every term in S is true. - /// - /// It turns out that this can also be expressed with set operations: - /// S contradicts t if and only if ⋂ S is disjoint with t - /// S contradicts t if and only if (⋂ S) ⋂ t = ∅ - #[cfg(test)] - fn contradicted_by(&self, terms_intersection: &Self) -> bool { - terms_intersection.intersection(self) == Self::empty() - } - - /// Check if a set of terms satisfies or contradicts a given term. - /// Otherwise the relation is inconclusive. - pub(crate) fn relation_with(&self, other_terms_intersection: &Self) -> Relation { - let full_intersection = self.intersection(other_terms_intersection); - if &full_intersection == other_terms_intersection { - Relation::Satisfied - } else if full_intersection == Self::empty() { - Relation::Contradicted - } else { - Relation::Inconclusive - } - } -} - -// impl AsRef> for Term { -impl AsRef for Term { - fn as_ref(&self) -> &Self { - self - } -} - -// REPORT ###################################################################### - -impl Display for Term { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Positive(set) => write!(f, "{}", set), - Self::Negative(set) => write!(f, "Not ( {} )", set), - } - } -} - -// TESTS ####################################################################### - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::range::Range; - use crate::version::NumberVersion; - use proptest::prelude::*; - - pub fn strategy() -> impl Strategy>> { - prop_oneof![ - crate::range::tests::strategy().prop_map(Term::Positive), - crate::range::tests::strategy().prop_map(Term::Negative), - ] - } - - proptest! { - - // Testing relation -------------------------------- - - #[test] - fn relation_with(term1 in strategy(), term2 in strategy()) { - match term1.relation_with(&term2) { - Relation::Satisfied => assert!(term1.satisfied_by(&term2)), - Relation::Contradicted => assert!(term1.contradicted_by(&term2)), - Relation::Inconclusive => { - assert!(!term1.satisfied_by(&term2)); - assert!(!term1.contradicted_by(&term2)); - } - } - } - - } -} diff --git a/src/type_aliases.rs b/src/type_aliases.rs index 1e5a944c..11ecf38d 100644 --- a/src/type_aliases.rs +++ b/src/type_aliases.rs @@ -2,8 +2,6 @@ //! Publicly exported type aliases. -use crate::range::Range; - /// Map implementation used by the library. pub type Map = rustc_hash::FxHashMap; @@ -17,4 +15,4 @@ pub type SelectedDependencies = Map; /// inside [DependencyConstraints] and [Dependencies::Unknown](crate::solver::Dependencies::Unknown): /// the former means the package has no dependencies and it is a known fact, /// while the latter means they could not be fetched by [DependencyProvider]. -pub type DependencyConstraints = Map>; +pub type DependencyConstraints = Map; diff --git a/src/type_aliases_bis.rs b/src/type_aliases_bis.rs deleted file mode 100644 index 11ecf38d..00000000 --- a/src/type_aliases_bis.rs +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 - -//! Publicly exported type aliases. - -/// Map implementation used by the library. -pub type Map = rustc_hash::FxHashMap; - -/// Concrete dependencies picked by the library during [resolve](crate::solver::resolve) -/// from [DependencyConstraints]. -pub type SelectedDependencies = Map; - -/// Subtype of [Dependencies] which holds information about -/// all possible versions a given package can accept. -/// There is a difference in semantics between an empty [Map>](Map) -/// inside [DependencyConstraints] and [Dependencies::Unknown](crate::solver::Dependencies::Unknown): -/// the former means the package has no dependencies and it is a known fact, -/// while the latter means they could not be fetched by [DependencyProvider]. -pub type DependencyConstraints = Map; From b31be342f99ab14b0e78d49c3e4b9d2dadb42b22 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sun, 8 Aug 2021 00:59:03 +0200 Subject: [PATCH 09/14] refactor: update tests to version_set --- examples/branching_error_reporting.rs | 4 +- examples/caching_dependency_provider.rs | 27 ++++++---- examples/doc_interface.rs | 4 +- examples/doc_interface_error.rs | 4 +- examples/doc_interface_semantic.rs | 4 +- examples/linear_error_reporting.rs | 4 +- src/lib.rs | 25 +++++---- src/solver.rs | 7 ++- tests/examples.rs | 13 +++-- tests/proptest.rs | 70 ++++++++++++------------- tests/sat_dependency_provider.rs | 16 +++--- tests/tests.rs | 8 +-- 12 files changed, 108 insertions(+), 78 deletions(-) diff --git a/examples/branching_error_reporting.rs b/examples/branching_error_reporting.rs index c1daa1bf..6642e000 100644 --- a/examples/branching_error_reporting.rs +++ b/examples/branching_error_reporting.rs @@ -6,9 +6,11 @@ use pubgrub::report::{DefaultStringReporter, Reporter}; use pubgrub::solver::{resolve, OfflineDependencyProvider}; use pubgrub::version::SemanticVersion; +type SemVS = Range; + // https://github.com/dart-lang/pub/blob/master/doc/solver.md#branching-error-reporting fn main() { - let mut dependency_provider = OfflineDependencyProvider::<&str, SemanticVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new(); #[rustfmt::skip] // root 1.0.0 depends on foo ^1.0.0 dependency_provider.add_dependencies( diff --git a/examples/caching_dependency_provider.rs b/examples/caching_dependency_provider.rs index bac730ea..72a3368f 100644 --- a/examples/caching_dependency_provider.rs +++ b/examples/caching_dependency_provider.rs @@ -6,16 +6,21 @@ use std::error::Error; use pubgrub::package::Package; use pubgrub::range::Range; use pubgrub::solver::{resolve, Dependencies, DependencyProvider, OfflineDependencyProvider}; -use pubgrub::version::{NumberVersion, Version}; +use pubgrub::version::NumberVersion; +use pubgrub::version_set::VersionSet; + +type NumVS = Range; // An example implementing caching dependency provider that will // store queried dependencies in memory and check them before querying more from remote. -struct CachingDependencyProvider> { +struct CachingDependencyProvider> { remote_dependencies: DP, - cached_dependencies: RefCell>, + cached_dependencies: RefCell>, } -impl> CachingDependencyProvider { +impl> + CachingDependencyProvider +{ pub fn new(remote_dependencies_provider: DP) -> Self { CachingDependencyProvider { remote_dependencies: remote_dependencies_provider, @@ -24,13 +29,13 @@ impl> CachingDependencyProv } } -impl> DependencyProvider - for CachingDependencyProvider +impl> DependencyProvider + for CachingDependencyProvider { - fn choose_package_version, U: std::borrow::Borrow>>( + fn choose_package_version, U: std::borrow::Borrow>( &self, packages: impl Iterator, - ) -> Result<(T, Option), Box> { + ) -> Result<(T, Option), Box> { self.remote_dependencies.choose_package_version(packages) } @@ -38,8 +43,8 @@ impl> DependencyProvider Result, Box> { + version: &VS::V, + ) -> Result, Box> { let mut cache = self.cached_dependencies.borrow_mut(); match cache.get_dependencies(package, version) { Ok(Dependencies::Unknown) => { @@ -65,7 +70,7 @@ impl> DependencyProvider::new(); + let mut remote_dependencies_provider = OfflineDependencyProvider::<&str, NumVS>::new(); // Add dependencies as needed. Here only root package is added. remote_dependencies_provider.add_dependencies("root", 1, Vec::new()); diff --git a/examples/doc_interface.rs b/examples/doc_interface.rs index b3e37838..4497705f 100644 --- a/examples/doc_interface.rs +++ b/examples/doc_interface.rs @@ -4,13 +4,15 @@ use pubgrub::range::Range; use pubgrub::solver::{resolve, OfflineDependencyProvider}; use pubgrub::version::NumberVersion; +type NumVS = Range; + // `root` depends on `menu` and `icons` // `menu` depends on `dropdown` // `dropdown` depends on `icons` // `icons` has no dependency #[rustfmt::skip] fn main() { - let mut dependency_provider = OfflineDependencyProvider::<&str, NumberVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new(); dependency_provider.add_dependencies( "root", 1, vec![("menu", Range::any()), ("icons", Range::any())], ); diff --git a/examples/doc_interface_error.rs b/examples/doc_interface_error.rs index 0ef0f1ec..2fadfd00 100644 --- a/examples/doc_interface_error.rs +++ b/examples/doc_interface_error.rs @@ -6,6 +6,8 @@ use pubgrub::report::{DefaultStringReporter, Reporter}; use pubgrub::solver::{resolve, OfflineDependencyProvider}; use pubgrub::version::SemanticVersion; +type SemVS = Range; + // `root` depends on `menu`, `icons 1.0.0` and `intl 5.0.0` // `menu 1.0.0` depends on `dropdown < 2.0.0` // `menu >= 1.1.0` depends on `dropdown >= 2.0.0` @@ -15,7 +17,7 @@ use pubgrub::version::SemanticVersion; // `intl` has no dependency #[rustfmt::skip] fn main() { - let mut dependency_provider = OfflineDependencyProvider::<&str, SemanticVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new(); // Direct dependencies: menu and icons. dependency_provider.add_dependencies("root", (1, 0, 0), vec![ ("menu", Range::any()), diff --git a/examples/doc_interface_semantic.rs b/examples/doc_interface_semantic.rs index b4c352e1..172f5db6 100644 --- a/examples/doc_interface_semantic.rs +++ b/examples/doc_interface_semantic.rs @@ -6,6 +6,8 @@ use pubgrub::report::{DefaultStringReporter, Reporter}; use pubgrub::solver::{resolve, OfflineDependencyProvider}; use pubgrub::version::SemanticVersion; +type SemVS = Range; + // `root` depends on `menu` and `icons 1.0.0` // `menu 1.0.0` depends on `dropdown < 2.0.0` // `menu >= 1.1.0` depends on `dropdown >= 2.0.0` @@ -14,7 +16,7 @@ use pubgrub::version::SemanticVersion; // `icons` has no dependency #[rustfmt::skip] fn main() { - let mut dependency_provider = OfflineDependencyProvider::<&str, SemanticVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new(); // Direct dependencies: menu and icons. dependency_provider.add_dependencies("root", (1, 0, 0), vec![ ("menu", Range::any()), diff --git a/examples/linear_error_reporting.rs b/examples/linear_error_reporting.rs index 0673fe36..aac6209a 100644 --- a/examples/linear_error_reporting.rs +++ b/examples/linear_error_reporting.rs @@ -6,9 +6,11 @@ use pubgrub::report::{DefaultStringReporter, Reporter}; use pubgrub::solver::{resolve, OfflineDependencyProvider}; use pubgrub::version::SemanticVersion; +type SemVS = Range; + // https://github.com/dart-lang/pub/blob/master/doc/solver.md#linear-error-reporting fn main() { - let mut dependency_provider = OfflineDependencyProvider::<&str, SemanticVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new(); #[rustfmt::skip] // root 1.0.0 depends on foo ^1.0.0 and baz ^1.0.0 dependency_provider.add_dependencies( diff --git a/src/lib.rs b/src/lib.rs index b0da98b0..6f2ea6de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,8 +49,10 @@ //! # use pubgrub::solver::{OfflineDependencyProvider, resolve}; //! # use pubgrub::version::NumberVersion; //! # use pubgrub::range::Range; -//! # -//! let mut dependency_provider = OfflineDependencyProvider::<&str, NumberVersion>::new(); +//! +//! type NumVS = Range; +//! +//! let mut dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new(); //! //! dependency_provider.add_dependencies( //! "root", 1, vec![("menu", Range::any()), ("icons", Range::any())], @@ -84,8 +86,10 @@ //! # //! # struct MyDependencyProvider; //! # -//! impl DependencyProvider for MyDependencyProvider { -//! fn choose_package_version, U: Borrow>>(&self,packages: impl Iterator) -> Result<(T, Option), Box> { +//! type SemVS = Range; +//! +//! impl DependencyProvider for MyDependencyProvider { +//! fn choose_package_version, U: Borrow>(&self,packages: impl Iterator) -> Result<(T, Option), Box> { //! unimplemented!() //! } //! @@ -93,7 +97,7 @@ //! &self, //! package: &String, //! version: &SemanticVersion, -//! ) -> Result, Box> { +//! ) -> Result, Box> { //! unimplemented!() //! } //! } @@ -153,13 +157,13 @@ //! [Output](crate::report::Reporter::Output) type and a single method. //! ``` //! # use pubgrub::package::Package; -//! # use pubgrub::version::Version; +//! # use pubgrub::version_set::VersionSet; //! # use pubgrub::report::DerivationTree; //! # -//! pub trait Reporter { +//! pub trait Reporter { //! type Output; //! -//! fn report(derivation_tree: &DerivationTree) -> Self::Output; +//! fn report(derivation_tree: &DerivationTree) -> Self::Output; //! } //! ``` //! Implementing a [Reporter](crate::report::Reporter) may involve a lot of heuristics @@ -173,8 +177,11 @@ //! # use pubgrub::report::{DefaultStringReporter, Reporter}; //! # use pubgrub::error::PubGrubError; //! # use pubgrub::version::NumberVersion; +//! # use pubgrub::range::Range; +//! # +//! # type NumVS = Range; //! # -//! # let dependency_provider = OfflineDependencyProvider::<&str, NumberVersion>::new(); +//! # let dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new(); //! # let root_package = "root"; //! # let root_version = 1; //! # diff --git a/src/solver.rs b/src/solver.rs index 4077aac4..a75b9367 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -44,9 +44,12 @@ //! # use pubgrub::solver::{resolve, OfflineDependencyProvider}; //! # use pubgrub::version::NumberVersion; //! # use pubgrub::error::PubGrubError; +//! # use pubgrub::range::Range; //! # -//! # fn try_main() -> Result<(), PubGrubError<&'static str, NumberVersion>> { -//! # let dependency_provider = OfflineDependencyProvider::<&str, NumberVersion>::new(); +//! # type NumVS = Range; +//! # +//! # fn try_main() -> Result<(), PubGrubError<&'static str, NumVS>> { +//! # let dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new(); //! # let package = "root"; //! # let version = 1; //! let solution = resolve(&dependency_provider, package, version)?; diff --git a/tests/examples.rs b/tests/examples.rs index 29901caf..96ac7bfd 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -5,10 +5,13 @@ use pubgrub::solver::{resolve, OfflineDependencyProvider}; use pubgrub::type_aliases::Map; use pubgrub::version::{NumberVersion, SemanticVersion}; +type NumVS = Range; +type SemVS = Range; + #[test] /// https://github.com/dart-lang/pub/blob/master/doc/solver.md#no-conflicts fn no_conflict() { - let mut dependency_provider = OfflineDependencyProvider::<&str, SemanticVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new(); #[rustfmt::skip] dependency_provider.add_dependencies( "root", (1, 0, 0), @@ -38,7 +41,7 @@ fn no_conflict() { #[test] /// https://github.com/dart-lang/pub/blob/master/doc/solver.md#avoiding-conflict-during-decision-making fn avoiding_conflict_during_decision_making() { - let mut dependency_provider = OfflineDependencyProvider::<&str, SemanticVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new(); #[rustfmt::skip] dependency_provider.add_dependencies( "root", (1, 0, 0), @@ -73,7 +76,7 @@ fn avoiding_conflict_during_decision_making() { #[test] /// https://github.com/dart-lang/pub/blob/master/doc/solver.md#performing-conflict-resolution fn conflict_resolution() { - let mut dependency_provider = OfflineDependencyProvider::<&str, SemanticVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new(); #[rustfmt::skip] dependency_provider.add_dependencies( "root", (1, 0, 0), @@ -106,7 +109,7 @@ fn conflict_resolution() { #[test] /// https://github.com/dart-lang/pub/blob/master/doc/solver.md#conflict-resolution-with-a-partial-satisfier fn conflict_with_partial_satisfier() { - let mut dependency_provider = OfflineDependencyProvider::<&str, SemanticVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new(); #[rustfmt::skip] // root 1.0.0 depends on foo ^1.0.0 and target ^2.0.0 dependency_provider.add_dependencies( @@ -171,7 +174,7 @@ fn conflict_with_partial_satisfier() { /// /// Solution: a0, b0, c0, d0 fn double_choices() { - let mut dependency_provider = OfflineDependencyProvider::<&str, NumberVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new(); dependency_provider.add_dependencies("a", 0, vec![("b", Range::any()), ("c", Range::any())]); dependency_provider.add_dependencies("b", 0, vec![("d", Range::exact(0))]); dependency_provider.add_dependencies("b", 1, vec![("d", Range::exact(1))]); diff --git a/tests/proptest.rs b/tests/proptest.rs index d2da3730..c59731f4 100644 --- a/tests/proptest.rs +++ b/tests/proptest.rs @@ -10,7 +10,8 @@ use pubgrub::solver::{ choose_package_with_fewest_versions, resolve, Dependencies, DependencyProvider, OfflineDependencyProvider, }; -use pubgrub::version::{NumberVersion, Version}; +use pubgrub::version::NumberVersion; +use pubgrub::version_set::VersionSet; use proptest::collection::{btree_map, vec}; use proptest::prelude::*; @@ -24,20 +25,24 @@ mod sat_dependency_provider; /// The same as [OfflineDependencyProvider] but takes versions from the opposite end: /// if [OfflineDependencyProvider] returns versions from newest to oldest, this returns them from oldest to newest. #[derive(Clone)] -struct OldestVersionsDependencyProvider(OfflineDependencyProvider); +struct OldestVersionsDependencyProvider( + OfflineDependencyProvider, +); -impl DependencyProvider for OldestVersionsDependencyProvider { - fn choose_package_version, U: std::borrow::Borrow>>( +impl DependencyProvider + for OldestVersionsDependencyProvider +{ + fn choose_package_version, U: std::borrow::Borrow>( &self, potential_packages: impl Iterator, - ) -> Result<(T, Option), Box> { + ) -> Result<(T, Option), Box> { Ok(choose_package_with_fewest_versions( |p| self.0.versions(p).into_iter().flatten().cloned(), potential_packages, )) } - fn get_dependencies(&self, p: &P, v: &V) -> Result, Box> { + fn get_dependencies(&self, p: &P, v: &VS::V) -> Result, Box> { self.0.get_dependencies(p, v) } } @@ -62,17 +67,17 @@ impl TimeoutDependencyProvider { } } -impl> DependencyProvider +impl> DependencyProvider for TimeoutDependencyProvider { - fn choose_package_version, U: std::borrow::Borrow>>( + fn choose_package_version, U: std::borrow::Borrow>( &self, potential_packages: impl Iterator, - ) -> Result<(T, Option), Box> { + ) -> Result<(T, Option), Box> { self.dp.choose_package_version(potential_packages) } - fn get_dependencies(&self, p: &P, v: &V) -> Result, Box> { + fn get_dependencies(&self, p: &P, v: &VS::V) -> Result, Box> { self.dp.get_dependencies(p, v) } @@ -85,10 +90,12 @@ impl> DependencyProvider; + #[test] #[should_panic] fn should_cancel_can_panic() { - let mut dependency_provider = OfflineDependencyProvider::<_, NumberVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<_, NumVS>::new(); dependency_provider.add_dependencies(0, 0, vec![(666, Range::any())]); // Run the algorithm. @@ -116,12 +123,7 @@ fn string_names() -> impl Strategy { pub fn registry_strategy( name: impl Strategy, bad_name: N, -) -> impl Strategy< - Value = ( - OfflineDependencyProvider, - Vec<(N, NumberVersion)>, - ), -> { +) -> impl Strategy, Vec<(N, NumberVersion)>)> { let max_crates = 40; let max_versions = 15; let shrinkage = 40; @@ -166,20 +168,18 @@ pub fn registry_strategy( ) .prop_map( move |(crate_vers_by_name, raw_dependencies, reverse_alphabetical, complicated_len)| { - let mut list_of_pkgid: Vec<( - (N, NumberVersion), - Option)>>, - )> = crate_vers_by_name - .iter() - .flat_map(|(name, vers)| { - vers.iter().map(move |x| { - ( - (name.clone(), NumberVersion::from(x.0)), - if x.1 { Some(vec![]) } else { None }, - ) + let mut list_of_pkgid: Vec<((N, NumberVersion), Option>)> = + crate_vers_by_name + .iter() + .flat_map(|(name, vers)| { + vers.iter().map(move |x| { + ( + (name.clone(), NumberVersion::from(x.0)), + if x.1 { Some(vec![]) } else { None }, + ) + }) }) - }) - .collect(); + .collect(); let len_all_pkgid = list_of_pkgid.len(); for (a, b, (c, d)) in raw_dependencies { let (a, b) = order_index(a, b, len_all_pkgid); @@ -210,7 +210,7 @@ pub fn registry_strategy( } } - let mut dependency_provider = OfflineDependencyProvider::::new(); + let mut dependency_provider = OfflineDependencyProvider::::new(); let complicated_len = std::cmp::min(complicated_len, list_of_pkgid.len()); let complicated: Vec<_> = if reverse_alphabetical { @@ -373,7 +373,7 @@ proptest! { .versions(package) .unwrap().collect(); let version = version_idx.get(&versions); - let dependencies: Vec<(u16, Range)> = match dependency_provider + let dependencies: Vec<(u16, NumVS)> = match dependency_provider .get_dependencies(package, version) .unwrap() { @@ -432,7 +432,7 @@ proptest! { Ok(used) => { // If resolution was successful, then unpublishing a version of a crate // that was not selected should not change that. - let mut smaller_dependency_provider = OfflineDependencyProvider::<_, NumberVersion>::new(); + let mut smaller_dependency_provider = OfflineDependencyProvider::<_, NumVS>::new(); for &(n, v) in &all_versions { if used.get(&n) == Some(&v) // it was used || to_remove.get(&(n, v)).is_none() // or it is not one to be removed @@ -455,7 +455,7 @@ proptest! { Err(_) => { // If resolution was unsuccessful, then it should stay unsuccessful // even if any version of a crate is unpublished. - let mut smaller_dependency_provider = OfflineDependencyProvider::<_, NumberVersion>::new(); + let mut smaller_dependency_provider = OfflineDependencyProvider::<_, NumVS>::new(); for &(n, v) in &all_versions { if to_remove.get(&(n, v)).is_none() // it is not one to be removed { @@ -488,7 +488,7 @@ fn large_case() { eprintln!("{}", name); let data = std::fs::read_to_string(&case).unwrap(); if name.ends_with("u16_NumberVersion.ron") { - let dependency_provider: OfflineDependencyProvider = + let dependency_provider: OfflineDependencyProvider = ron::de::from_str(&data).unwrap(); let mut sat = SatResolve::new(&dependency_provider); for p in dependency_provider.packages() { diff --git a/tests/sat_dependency_provider.rs b/tests/sat_dependency_provider.rs index fa964ca8..f85681be 100644 --- a/tests/sat_dependency_provider.rs +++ b/tests/sat_dependency_provider.rs @@ -3,7 +3,7 @@ use pubgrub::package::Package; use pubgrub::solver::{Dependencies, DependencyProvider, OfflineDependencyProvider}; use pubgrub::type_aliases::{Map, SelectedDependencies}; -use pubgrub::version::Version; +use pubgrub::version_set::VersionSet; use varisat::ExtendFormula; const fn num_bits() -> usize { @@ -46,17 +46,17 @@ fn sat_at_most_one(solver: &mut impl varisat::ExtendFormula, vars: &[varisat::Va /// /// The SAT library does not optimize for the newer version, /// so the selected packages may not match the real resolver. -pub struct SatResolve { +pub struct SatResolve { solver: varisat::Solver<'static>, - all_versions_by_p: Map>, + all_versions_by_p: Map>, } -impl SatResolve { - pub fn new(dp: &OfflineDependencyProvider) -> Self { +impl SatResolve { + pub fn new(dp: &OfflineDependencyProvider) -> Self { let mut cnf = varisat::CnfFormula::new(); let mut all_versions = vec![]; - let mut all_versions_by_p: Map> = Map::default(); + let mut all_versions_by_p: Map> = Map::default(); for p in dp.packages() { let mut versions_for_p = vec![]; @@ -110,7 +110,7 @@ impl SatResolve { } } - pub fn sat_resolve(&mut self, name: &P, ver: &V) -> bool { + pub fn sat_resolve(&mut self, name: &P, ver: &VS::V) -> bool { if let Some(vers) = self.all_versions_by_p.get(name) { if let Some((_, var)) = vers.iter().find(|(v, _)| v == ver) { self.solver.assume(&[var.positive()]); @@ -126,7 +126,7 @@ impl SatResolve { } } - pub fn sat_is_valid_solution(&mut self, pids: &SelectedDependencies) -> bool { + pub fn sat_is_valid_solution(&mut self, pids: &SelectedDependencies) -> bool { let mut assumption = vec![]; for (p, vs) in &self.all_versions_by_p { diff --git a/tests/tests.rs b/tests/tests.rs index 7aeed03b..dff08935 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -5,9 +5,11 @@ use pubgrub::range::Range; use pubgrub::solver::{resolve, OfflineDependencyProvider}; use pubgrub::version::NumberVersion; +type NumVS = Range; + #[test] fn same_result_on_repeated_runs() { - let mut dependency_provider = OfflineDependencyProvider::<_, NumberVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<_, NumVS>::new(); dependency_provider.add_dependencies("c", 0, vec![]); dependency_provider.add_dependencies("c", 2, vec![]); @@ -29,7 +31,7 @@ fn same_result_on_repeated_runs() { #[test] fn should_always_find_a_satisfier() { - let mut dependency_provider = OfflineDependencyProvider::<_, NumberVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<_, NumVS>::new(); dependency_provider.add_dependencies("a", 0, vec![("b", Range::none())]); assert!(matches!( resolve(&dependency_provider, "a", 0), @@ -45,7 +47,7 @@ fn should_always_find_a_satisfier() { #[test] fn cannot_depend_on_self() { - let mut dependency_provider = OfflineDependencyProvider::<_, NumberVersion>::new(); + let mut dependency_provider = OfflineDependencyProvider::<_, NumVS>::new(); dependency_provider.add_dependencies("a", 0, vec![("a", Range::any())]); assert!(matches!( resolve(&dependency_provider, "a", 0), From c132146f1d3596094ae8eff13bb5ce647126cd91 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sun, 8 Aug 2021 13:07:38 +0200 Subject: [PATCH 10/14] feat: add serde bounds to OfflineDependencyProvider --- src/solver.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/solver.rs b/src/solver.rs index a75b9367..de2170fa 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -295,6 +295,13 @@ where /// A basic implementation of [DependencyProvider]. #[derive(Debug, Clone, Default)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serde", + serde(bound( + serialize = "VS::V: serde::Serialize, VS: serde::Serialize, P: serde::Serialize", + deserialize = "VS::V: serde::Deserialize<'de>, VS: serde::Deserialize<'de>, P: serde::Deserialize<'de>" + )) +)] #[cfg_attr(feature = "serde", serde(transparent))] pub struct OfflineDependencyProvider { dependencies: Map>>, From 782f339ab64445fe1102ea86e174f3561e0f6501 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sun, 8 Aug 2021 13:17:04 +0200 Subject: [PATCH 11/14] refactor: update proptest.rs to VersionSet --- tests/proptest.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/proptest.rs b/tests/proptest.rs index c59731f4..9fdd272d 100644 --- a/tests/proptest.rs +++ b/tests/proptest.rs @@ -10,7 +10,7 @@ use pubgrub::solver::{ choose_package_with_fewest_versions, resolve, Dependencies, DependencyProvider, OfflineDependencyProvider, }; -use pubgrub::version::NumberVersion; +use pubgrub::version::{NumberVersion, SemanticVersion}; use pubgrub::version_set::VersionSet; use proptest::collection::{btree_map, vec}; @@ -91,6 +91,7 @@ impl> DependencyProvid } type NumVS = Range; +type SemVS = Range; #[test] #[should_panic] @@ -501,10 +502,8 @@ fn large_case() { } } } else if name.ends_with("str_SemanticVersion.ron") { - let dependency_provider: OfflineDependencyProvider< - &str, - pubgrub::version::SemanticVersion, - > = ron::de::from_str(&data).unwrap(); + let dependency_provider: OfflineDependencyProvider<&str, SemVS> = + ron::de::from_str(&data).unwrap(); let mut sat = SatResolve::new(&dependency_provider); for p in dependency_provider.packages() { for n in dependency_provider.versions(p).unwrap() { From 2380199f28f97f63c36cb335596b0775508ef59a Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sun, 8 Aug 2021 18:44:04 +0200 Subject: [PATCH 12/14] docs: fix links --- src/solver.rs | 4 ++-- src/type_aliases.rs | 7 +++---- src/version_set.rs | 26 +++++++++++++++----------- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/solver.rs b/src/solver.rs index de2170fa..3099c5db 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -203,7 +203,7 @@ pub fn resolve( } /// An enum used by [DependencyProvider] that holds information about package dependencies. -/// For each [Package] there is a [Range] of concrete versions it allows as a dependency. +/// For each [Package] there is a set of versions allowed as a dependency. #[derive(Clone)] pub enum Dependencies { /// Package dependencies are unavailable. @@ -219,7 +219,7 @@ pub trait DependencyProvider { /// is the process of choosing the next package /// and version that will be appended to the partial solution. /// Every time such a decision must be made, - /// potential valid packages and version ranges are preselected by the resolver, + /// potential valid packages and sets of versions are preselected by the resolver, /// and the dependency provider must choose. /// /// The strategy employed to choose such package and version diff --git a/src/type_aliases.rs b/src/type_aliases.rs index 11ecf38d..6a76d016 100644 --- a/src/type_aliases.rs +++ b/src/type_aliases.rs @@ -9,10 +9,9 @@ pub type Map = rustc_hash::FxHashMap; /// from [DependencyConstraints]. pub type SelectedDependencies = Map; -/// Subtype of [Dependencies] which holds information about -/// all possible versions a given package can accept. -/// There is a difference in semantics between an empty [Map>](Map) +/// Holds information about all possible versions a given package can accept. +/// There is a difference in semantics between an empty map /// inside [DependencyConstraints] and [Dependencies::Unknown](crate::solver::Dependencies::Unknown): /// the former means the package has no dependencies and it is a known fact, -/// while the latter means they could not be fetched by [DependencyProvider]. +/// while the latter means they could not be fetched by [DependencyProvider](crate::solver::DependencyProvider). pub type DependencyConstraints = Map; diff --git a/src/version_set.rs b/src/version_set.rs index 8cd1be47..007e2c66 100644 --- a/src/version_set.rs +++ b/src/version_set.rs @@ -1,18 +1,22 @@ // SPDX-License-Identifier: MPL-2.0 -//! Ranges are constraints defining sets of versions. +//! As its name suggests, the [VersionSet] trait describes sets of versions. //! -//! Concretely, those constraints correspond to any set of versions -//! representable as the concatenation, union, and complement -//! of the ranges building blocks. +//! One needs to define +//! - the associate type for versions, +//! - two constructors for the empty set and a singleton set, +//! - the complement and intersection set operations, +//! - and a function to evaluate membership of versions. //! -//! Those building blocks are: -//! - [none()](Range::none): the empty set -//! - [any()](Range::any): the set of all possible versions -//! - [exact(v)](Range::exact): the set containing only the version v -//! - [higher_than(v)](Range::higher_than): the set defined by `v <= versions` -//! - [strictly_lower_than(v)](Range::strictly_lower_than): the set defined by `versions < v` -//! - [between(v1, v2)](Range::between): the set defined by `v1 <= versions < v2` +//! Two functions are automatically derived, thanks to the mathematical properties of sets. +//! You can overwrite those implementations, but we highly recommend that you don't, +//! except if you are confident in a correct implementation that brings much performance gains. +//! +//! It is also extremely important that the `Eq` trait is correctly implemented. +//! In particular, you can only use `#[derive(Eq, PartialEq)]` if `Eq` is strictly equivalent to the +//! structural equality, i.e. if versions have canonical representations. +//! Such problems may arise if your implementations of `complement()` and `intersection()` do not +//! return canonical representations so be careful there. use std::fmt::{Debug, Display}; From 7727938886fd3598f29cc2c8eb06921c121aaa9d Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sun, 8 Aug 2021 19:15:16 +0200 Subject: [PATCH 13/14] refactor: allow clippy type_complexity --- src/solver.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/solver.rs b/src/solver.rs index 3c8e85d4..05dc37e2 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -239,6 +239,7 @@ pub trait DependencyProvider { /// of the available versions in preference order for any package. /// /// Note: the type `T` ensures that this returns an item from the `packages` argument. + #[allow(clippy::type_complexity)] fn choose_package_version, U: Borrow>( &self, potential_packages: impl Iterator, @@ -363,6 +364,7 @@ impl OfflineDependencyProvider { /// Packages are picked with the fewest versions contained in the constraints first. /// Versions are picked with the newest versions first. impl DependencyProvider for OfflineDependencyProvider { + #[allow(clippy::type_complexity)] fn choose_package_version, U: Borrow>( &self, potential_packages: impl Iterator, From 8c2cc40b974fdfb390e408b0c1638e86edab88b5 Mon Sep 17 00:00:00 2001 From: Matthieu Pizenberg Date: Sat, 21 May 2022 12:03:23 +0200 Subject: [PATCH 14/14] Small docs changes --- src/type_aliases.rs | 4 ++-- src/version_set.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/type_aliases.rs b/src/type_aliases.rs index 6a76d016..11cc37c7 100644 --- a/src/type_aliases.rs +++ b/src/type_aliases.rs @@ -12,6 +12,6 @@ pub type SelectedDependencies = Map; /// Holds information about all possible versions a given package can accept. /// There is a difference in semantics between an empty map /// inside [DependencyConstraints] and [Dependencies::Unknown](crate::solver::Dependencies::Unknown): -/// the former means the package has no dependencies and it is a known fact, -/// while the latter means they could not be fetched by [DependencyProvider](crate::solver::DependencyProvider). +/// the former means the package has no dependency and it is a known fact, +/// while the latter means they could not be fetched by the [DependencyProvider](crate::solver::DependencyProvider). pub type DependencyConstraints = Map; diff --git a/src/version_set.rs b/src/version_set.rs index 007e2c66..501ec700 100644 --- a/src/version_set.rs +++ b/src/version_set.rs @@ -14,7 +14,7 @@ //! //! It is also extremely important that the `Eq` trait is correctly implemented. //! In particular, you can only use `#[derive(Eq, PartialEq)]` if `Eq` is strictly equivalent to the -//! structural equality, i.e. if versions have canonical representations. +//! structural equality, i.e. if version sets have canonical representations. //! Such problems may arise if your implementations of `complement()` and `intersection()` do not //! return canonical representations so be careful there.