diff --git a/src/experimental/helpers.rs b/src/experimental/helpers.rs new file mode 100644 index 00000000..5a867907 --- /dev/null +++ b/src/experimental/helpers.rs @@ -0,0 +1,355 @@ +//! Helpers structs. + +use std::fmt::{self, Display}; +use std::iter::repeat_n; +use std::num::NonZeroU64; +use std::rc::Rc; + +use crate::experimental::{VersionIndex, VersionSet}; + +/// Package allowing more than 63 versions. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct Pkg

{ + pkg: P, + quotient: u64, + count: u64, +} + +impl

Pkg

{ + /// Get the inner package. + pub fn pkg(&self) -> &P { + &self.pkg + } +} + +impl Display for Pkg

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{} (q={})", self.pkg, self.quotient) + } +} + +/// Virtual package ensuring package unicity. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct VirtualPkg

{ + pkg: P, + quotient: u64, + count: u64, +} + +impl

VirtualPkg

{ + /// Get the inner package. + pub fn pkg(&self) -> &P { + &self.pkg + } +} + +impl Display for VirtualPkg

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "VirtualPkg({}, q={}, c={})", + self.pkg, self.quotient, self.count + ) + } +} + +/// Virtual package dependency allowing more than 63 versions. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub struct VirtualDep

{ + pkg: P, + version_indices: Rc<[VersionSet]>, + offset: u64, + quotient: u64, +} + +impl

VirtualDep

{ + /// Get the inner package. + pub fn pkg(&self) -> &P { + &self.pkg + } +} + +impl Display for VirtualDep

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let vc = self + .version_indices + .iter() + .map(|vs| vs.count()) + .sum::(); + + write!( + f, + "VirtualDep({}, vc={vc}, o={}, q={})", + self.pkg, self.offset, self.quotient + ) + } +} + +/// Package wrapper used to allow more than 63 versions per package. +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum PackageVersionWrapper { + /// Package allowing more than 63 versions + Pkg(Pkg

), + /// Virtual package ensuring package unicity + VirtualPkg(VirtualPkg

), + /// Virtual package dependency allowing more than 63 versions + VirtualDep(VirtualDep

), +} + +impl Display for PackageVersionWrapper

{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Pkg(p) => p.fmt(f), + Self::VirtualPkg(vp) => vp.fmt(f), + Self::VirtualDep(vd) => vd.fmt(f), + } + } +} + +impl PackageVersionWrapper

{ + /// Create a new package. + pub fn new_pkg( + pkg: P, + true_version_index: u64, + version_count: NonZeroU64, + ) -> (Self, VersionIndex) { + ( + Self::Pkg(Pkg { + pkg, + quotient: true_version_index / VersionIndex::MAX, + count: (version_count.get() - 1) / VersionIndex::MAX, + }), + VersionIndex::new((true_version_index % VersionIndex::MAX) as u8).unwrap(), + ) + } + + /// Create a new package dependency with no versions. + pub fn new_empty_dep(pkg: P) -> (Self, VersionSet) { + ( + Self::Pkg(Pkg { + pkg, + quotient: 0, + count: 0, + }), + VersionSet::empty(), + ) + } + + /// Create a new package dependency at the specified version. + pub fn new_singleton_dep( + pkg: P, + true_version_index: u64, + version_count: u64, + ) -> (Self, VersionSet) { + match NonZeroU64::new(version_count) { + Some(version_count) => { + assert!(true_version_index < version_count.get()); + let (this, v) = Self::new_pkg(pkg, true_version_index, version_count); + (this, VersionSet::singleton(v)) + } + None => Self::new_empty_dep(pkg), + } + } + + /// Create a new package dependency at the specified versions. + pub fn new_dep( + pkg: P, + true_version_indices: impl IntoIterator, + version_count: u64, + ) -> (Self, VersionSet) { + let Some(nz_version_count) = NonZeroU64::new(version_count) else { + return Self::new_empty_dep(pkg); + }; + if version_count <= VersionIndex::MAX { + let mut set = VersionSet::empty(); + for true_version_index in true_version_indices { + assert!(true_version_index < version_count); + let v = VersionIndex::new(true_version_index as u8).unwrap(); + set = set.union(VersionSet::singleton(v)); + } + return ( + Self::Pkg(Pkg { + pkg, + quotient: 0, + count: (version_count - 1) / VersionIndex::MAX, + }), + set, + ); + } + + let mut true_version_indices = true_version_indices.into_iter(); + + let Some(first) = true_version_indices.next() else { + return Self::new_empty_dep(pkg); + }; + assert!(first < version_count); + + let Some(second) = true_version_indices.next() else { + let (d, vs) = Self::new_pkg(pkg, first, nz_version_count); + return (d, VersionSet::singleton(vs)); + }; + assert!(second < version_count); + + let mut version_indices = Rc::from_iter(repeat_n( + VersionSet::empty(), + (1 + (version_count - 1) / VersionIndex::MAX) as usize, + )); + let versions_slice = Rc::make_mut(&mut version_indices); + + for true_version_index in [first, second].into_iter().chain(true_version_indices) { + assert!(true_version_index < version_count); + let index = (true_version_index / VersionIndex::MAX) as usize; + let v = VersionIndex::new((true_version_index % VersionIndex::MAX) as u8).unwrap(); + let set = versions_slice.get_mut(index).unwrap(); + *set = set.union(VersionSet::singleton(v)); + } + + let offset = 0; + let quotient = VersionIndex::MAX.pow(version_count.ilog(VersionIndex::MAX) - 1); + let version_set = Self::dep_version_set(&version_indices, offset, quotient); + + let this = Self::VirtualDep(VirtualDep { + pkg, + version_indices, + offset, + quotient, + }); + + (this, version_set) + } + + /// Clone and replace the package of this wrapper. + pub fn replace_pkg(&self, new_pkg: T) -> PackageVersionWrapper { + match *self { + Self::Pkg(Pkg { + pkg: _, + quotient, + count, + }) => PackageVersionWrapper::Pkg(Pkg { + pkg: new_pkg, + quotient, + count, + }), + Self::VirtualPkg(VirtualPkg { + pkg: _, + quotient, + count, + }) => PackageVersionWrapper::VirtualPkg(VirtualPkg { + pkg: new_pkg, + quotient, + count, + }), + Self::VirtualDep(VirtualDep { + pkg: _, + ref version_indices, + offset, + quotient, + }) => PackageVersionWrapper::VirtualDep(VirtualDep { + pkg: new_pkg, + version_indices: version_indices.clone(), + offset, + quotient, + }), + } + } + + /// Get the inner package if existing. + pub fn inner_pkg(&self) -> Option<&P> { + match self { + Self::Pkg(Pkg { pkg, .. }) => Some(pkg), + _ => None, + } + } + + /// Get the inner package if existing. + pub fn inner(&self, version_index: VersionIndex) -> Option<(&P, u64)> { + match self { + Self::Pkg(Pkg { pkg, quotient, .. }) => Some(( + pkg, + quotient * VersionIndex::MAX + version_index.get() as u64, + )), + _ => None, + } + } + + /// Get the inner package if existing. + pub fn into_inner(self, version_index: VersionIndex) -> Option<(P, u64)> { + match self { + Self::Pkg(Pkg { pkg, quotient, .. }) => Some(( + pkg, + quotient * VersionIndex::MAX + version_index.get() as u64, + )), + _ => None, + } + } + + /// Get the wrapper virtual dependency if existing. + pub fn dependency(&self, version_index: VersionIndex) -> Option<(Self, VersionSet)> { + match *self { + Self::Pkg(Pkg { + ref pkg, + quotient, + count, + }) + | Self::VirtualPkg(VirtualPkg { + ref pkg, + quotient, + count, + }) => { + if count == 0 { + None + } else { + Some(( + Self::VirtualPkg(VirtualPkg { + pkg: pkg.clone(), + quotient: quotient / VersionIndex::MAX, + count: count / VersionIndex::MAX, + }), + VersionSet::singleton( + VersionIndex::new((quotient % VersionIndex::MAX) as u8).unwrap(), + ), + )) + } + } + Self::VirtualDep(VirtualDep { + ref pkg, + ref version_indices, + offset, + quotient, + }) => { + let offset = offset + version_index.get() as u64 * quotient; + if quotient == 1 { + return Some(( + Self::Pkg(Pkg { + pkg: pkg.clone(), + quotient: offset, + count: (version_indices.len() - 1) as u64, + }), + version_indices[offset as usize], + )); + } + let quotient = quotient / VersionIndex::MAX; + let version_set = Self::dep_version_set(version_indices, offset, quotient); + + let this = Self::VirtualDep(VirtualDep { + pkg: pkg.clone(), + version_indices: version_indices.clone(), + offset, + quotient, + }); + + Some((this, version_set)) + } + } + } + + fn dep_version_set(sets: &[VersionSet], offset: u64, quotient: u64) -> VersionSet { + sets[offset as usize..] + .chunks(quotient as usize) + .take(VersionIndex::MAX as usize) + .enumerate() + .filter(|&(_, sets)| sets.iter().any(|&vs| vs != VersionSet::empty())) + .map(|(i, _)| VersionSet::singleton(VersionIndex::new(i as u8).unwrap())) + .fold(VersionSet::empty(), |acc, vs| acc.union(vs)) + } +} diff --git a/src/experimental/mod.rs b/src/experimental/mod.rs new file mode 100644 index 00000000..7f7fab5e --- /dev/null +++ b/src/experimental/mod.rs @@ -0,0 +1,6 @@ +pub mod helpers; +mod term; +mod version; + +pub use term::Term; +pub use version::{VersionIndex, VersionSet}; diff --git a/src/experimental/term.rs b/src/experimental/term.rs new file mode 100644 index 00000000..4125b9dd --- /dev/null +++ b/src/experimental/term.rs @@ -0,0 +1,280 @@ +// 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 std::fmt::{self, Display}; + +use crate::experimental::{VersionIndex, VersionSet}; + +/// A positive or negative expression regarding a set of versions. +/// +/// `Term::positive(vs)` and `Term::negative(vs.complement())` are not equivalent: +/// * `Term::positive(vs)` is satisfied if the package is selected AND the selected version is in `vs`. +/// * `Term::negative(vs.complement())` is satisfied if the package is not selected OR the selected version is in `vs`. +/// +/// A positive term in the partial solution requires a version to be selected, but a negative term +/// allows for a solution that does not have that package selected. +/// Specifically, `Term::positive(VersionSet::empty())` means that there was a conflict +/// (we need to select a version for the package but can't pick any), +/// while `Term::negative(VersionSet::full())` would mean it is fine as long as we don't select the package. +/// +/// Like [`VersionSet`], this is implemented as a `u64` bitset, +/// where the first bit is set if the unselected package is allowed (for a negative term), +/// and the others bits are set if the package selected at the corresponding version index is allowed. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +#[repr(transparent)] +pub struct Term(u64); + +impl Term { + /// Construct a positive `Term`. + /// For example, `[1, 2]` is a positive expression + /// that is evaluated true if a version with index 1 or 2 is selected. + #[inline] + pub(crate) fn positive(vs: VersionSet) -> Self { + Self(vs.0) + } + + /// Construct a negative `Term`. + /// For example, `not [3, 5]` is a negative expression + /// that is evaluated true if a version with index 3 or 5 is selected + /// or if no version is selected at all. + #[inline] + pub(crate) fn negative(vs: VersionSet) -> Self { + Self(!vs.0) + } + + /// A term that is always true. + #[inline] + pub(crate) fn any() -> Self { + Self(!0) + } + + /// A term that is never true. + #[inline] + pub(crate) fn empty() -> Self { + Self(0) + } + + /// A positive term containing exactly that version. + #[inline] + pub(crate) fn exact(version_index: VersionIndex) -> Self { + Self::positive(VersionSet::singleton(version_index)) + } + + /// Simply check if a term is positive. + #[inline] + pub fn is_positive(self) -> bool { + self.0 & 1 == 0 + } + + /// Simply check if a term is negative. + #[inline] + pub fn is_negative(self) -> bool { + self.0 & 1 != 0 + } + + /// Negate a term. + /// Evaluation of a negated term always returns + /// the opposite of the evaluation of the original one. + #[inline] + pub(crate) fn negate(self) -> Self { + Self(!self.0) + } + + /// Get the inner version set. + #[inline] + pub fn version_set(self) -> VersionSet { + if self.is_positive() { + VersionSet(self.0) + } else { + VersionSet(!self.0) + } + } + + /// Evaluate a term regarding a given choice of version. + #[inline] + pub(crate) fn contains(self, v: VersionIndex) -> bool { + self.0 & VersionSet::singleton(v).0 != 0 + } + + /// Unwrap the set contained in a positive term. + /// Will panic if used on a negative set. + #[inline] + pub(crate) fn unwrap_positive(self) -> VersionSet { + if self.is_positive() { + VersionSet(self.0) + } else { + panic!("Negative term cannot unwrap positive set") + } + } + + /// Unwrap the set contained in a negative term. + /// Will panic if used on a positive set. + #[inline] + pub(crate) fn unwrap_negative(self) -> VersionSet { + if self.is_negative() { + VersionSet(!self.0) + } else { + panic!("Positive term cannot unwrap negative set") + } + } + + /// Compute the intersection of two terms. + /// The intersection is negative (unselected package is allowed) + /// if all terms are negative. + #[inline] + pub(crate) fn intersection(self, other: Self) -> Self { + Self(self.0 & other.0) + } + + /// Compute the union of two terms. + /// If at least one term is negative, the union is also negative (unselected package is allowed). + #[inline] + pub(crate) fn union(self, other: Self) -> Self { + Self(self.0 | other.0) + } + + /// Check whether two terms are mutually exclusive. + #[inline] + pub(crate) fn is_disjoint(self, other: Self) -> bool { + self.0 & other.0 == 0 + } + + /// 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. + #[inline] + pub(crate) fn subset_of(self, other: Self) -> bool { + self.0 & other.0 == self.0 + } + + /// Check if a set of terms satisfies or contradicts a given term. + /// Otherwise the relation is inconclusive. + #[inline] + pub(crate) fn relation_with(self, other_terms_intersection: Self) -> Relation { + if other_terms_intersection.subset_of(self) { + Relation::Satisfied + } else if other_terms_intersection.is_disjoint(self) { + Relation::Contradicted + } else { + Relation::Inconclusive + } + } +} + +/// 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, +} + +impl Display for Term { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_negative() { + write!(f, "Not ( ")?; + } + + let mut list = f.debug_list(); + for v in self.version_set().iter() { + list.entry(&v.get()); + } + list.finish()?; + + if self.is_negative() { + write!(f, " )")?; + } + + Ok(()) + } +} + +#[cfg(test)] +pub mod tests { + use proptest::prelude::*; + + use super::*; + + 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 + 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 = ∅ + fn contradicted_by(self, terms_intersection: Self) -> bool { + terms_intersection.intersection(self) == Self::empty() + } + } + + pub fn strategy() -> impl Strategy { + any::().prop_map(Term) + } + + 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)); + } + } + } + + /// Ensure that we don't wrongly convert between positive and negative ranges + #[test] + fn positive_negative(term1 in strategy(), term2 in strategy()) { + let intersection_positive = term1.is_positive() || term2.is_positive(); + let union_positive = term1.is_positive() & term2.is_positive(); + assert_eq!(term1.intersection(term2).is_positive(), intersection_positive); + assert_eq!(term1.union(term2).is_positive(), union_positive); + } + + #[test] + fn is_disjoint_through_intersection(r1 in strategy(), r2 in strategy()) { + let disjoint_def = r1.intersection(r2) == Term::empty(); + assert_eq!(r1.is_disjoint(r2), disjoint_def); + } + + #[test] + fn subset_of_through_intersection(r1 in strategy(), r2 in strategy()) { + let disjoint_def = r1.intersection(r2) == r1; + assert_eq!(r1.subset_of(r2), disjoint_def); + } + + #[test] + fn union_through_intersection(r1 in strategy(), r2 in strategy()) { + let union_def = r1 + .negate() + .intersection(r2.negate()) + .negate(); + assert_eq!(r1.union(r2), union_def); + } + } +} diff --git a/src/experimental/version.rs b/src/experimental/version.rs new file mode 100644 index 00000000..c76d6c73 --- /dev/null +++ b/src/experimental/version.rs @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: MPL-2.0 + +/// Type for identifying a version index. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +#[repr(transparent)] +pub struct VersionIndex(u8); + +impl VersionIndex { + /// Maximum possible version index. + pub const MAX: u64 = (u64::BITS - 1) as u64; + + /// Constructor for a version index. + #[inline] + pub fn new(v: u8) -> Option { + if v < Self::MAX as u8 { + Some(Self(v)) + } else { + None + } + } + + /// Get the inner version index. + #[inline] + pub fn get(self) -> u8 { + self.0 + } +} + +/// Type for identifying a set of version indices. +/// +/// This is implemented as a `u64` bitset which can represent up to 63 versions. +/// The first bit is kept unset to leave space for the positive/negative bit of [`Term`](super::Term). +/// +/// See the [helpers](super::helpers) module to support more than 63 versions by using a wrapper package. +#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] +#[repr(transparent)] +pub struct VersionSet(pub(crate) u64); + +impl VersionSet { + /// Constructor for an empty set containing no version index. + #[inline] + pub fn empty() -> Self { + Self(0) + } + + /// Constructor for the set containing all version indices. + #[inline] + pub fn full() -> Self { + Self(u64::MAX & (!1)) + } + + /// Constructor for a set containing exactly one version index. + #[inline] + pub fn singleton(v: VersionIndex) -> Self { + Self(2 << v.0) + } + + /// Compute the complement of this set. + #[inline] + pub fn complement(self) -> Self { + Self((!self.0) & (!1)) + } + + /// Compute the intersection with another set. + #[inline] + pub fn intersection(self, other: Self) -> Self { + Self(self.0 & other.0) + } + + /// Compute the union with another set. + #[inline] + pub fn union(self, other: Self) -> Self { + Self(self.0 | other.0) + } + + /// Evaluate membership of a version index in this set. + #[inline] + pub fn contains(self, v: VersionIndex) -> bool { + self.intersection(Self::singleton(v)) != Self::empty() + } + + /// Whether the set has no overlapping version indices. + #[inline] + pub fn is_disjoint(self, other: Self) -> bool { + self.intersection(other) == Self::empty() + } + + /// Whether all version indices of `self` are contained in `other`. + #[inline] + pub fn subset_of(self, other: Self) -> bool { + self == self.intersection(other) + } + + /// Get an iterator over the version indices contained in the set. + #[inline] + pub fn iter(self) -> impl Iterator { + (0..VersionIndex::MAX) + .filter(move |v| self.0 & (2 << v) != 0) + .map(|v| VersionIndex(v as u8)) + } + + /// Get the first version index of the set. + #[inline] + pub fn first(self) -> Option { + if self != Self::empty() { + Some(VersionIndex((self.0 >> 1).trailing_zeros() as u8)) + } else { + None + } + } + + /// Get the last version index of the set. + #[inline] + pub fn last(self) -> Option { + if self != Self::empty() { + let v = VersionIndex::MAX - (self.0 >> 1).leading_zeros() as u64; + Some(VersionIndex(v as u8)) + } else { + None + } + } + + /// Count the number of version indices contained in the set. + #[inline] + pub fn count(self) -> usize { + self.0.count_ones() as usize + } +} diff --git a/src/lib.rs b/src/lib.rs index cc1c943f..14ee1276 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -211,6 +211,8 @@ #![warn(missing_docs)] mod error; +#[allow(unused)] +mod experimental; mod package; mod provider; mod report;