Upgrade PubGrub to dev branch (#147)

Updates to `29c48fb9f3daa11bd02794edd55060d0b01ee705` from the
`pubgrub-rs` dev branch. This lets us reduce the number of changes we've
made to PubGrub itself (now, only changing visibility to export a few
things from the `solver.rs` module).
This commit is contained in:
Charlie Marsh 2023-10-19 23:23:26 -04:00 committed by GitHub
parent bcd281eb1f
commit 9b3405bf0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 1416 additions and 865 deletions

1
Cargo.lock generated
View file

@ -1967,6 +1967,7 @@ dependencies = [
name = "pubgrub"
version = "0.2.1"
dependencies = [
"log",
"rustc-hash",
"thiserror",
]

View file

@ -1,3 +1,4 @@
use pubgrub::range::Range;
use thiserror::Error;
use pep508_rs::Requirement;
@ -23,7 +24,7 @@ pub enum ResolveError {
Join(#[from] tokio::task::JoinError),
#[error(transparent)]
PubGrub(#[from] pubgrub::error::PubGrubError<PubGrubPackage, PubGrubVersion>),
PubGrub(#[from] pubgrub::error::PubGrubError<PubGrubPackage, Range<PubGrubVersion>>),
}
impl<T> From<futures::channel::mpsc::TrySendError<T>> for ResolveError {

View file

@ -58,19 +58,19 @@ pub(crate) fn version_range(
specifiers: Option<&pep508_rs::VersionOrUrl>,
) -> Result<Range<PubGrubVersion>> {
let Some(specifiers) = specifiers else {
return Ok(Range::any());
return Ok(Range::full());
};
let pep508_rs::VersionOrUrl::VersionSpecifier(specifiers) = specifiers else {
return Ok(Range::any());
return Ok(Range::full());
};
let mut final_range = Range::any();
let mut final_range = Range::full();
for spec in specifiers.iter() {
let spec_range =
PubGrubSpecifier::try_from(spec)?
.into_iter()
.fold(Range::none(), |accum, range| {
.fold(Range::empty(), |accum, range| {
accum.union(&if range.end < *MAX_VERSION {
Range::between(range.start, range.end)
} else {

View file

@ -37,6 +37,13 @@ impl pubgrub::version::Version for PubGrubVersion {
self.next()
}
}
// impl From<PubGrubVersion> for Range<PubGrubVersion> {
// fn from(value: PubGrubVersion) -> Self {
// Range::from(value)
// }
// }
impl<'a> From<&'a PubGrubVersion> for &'a pep440_rs::Version {
fn from(version: &'a PubGrubVersion) -> Self {
&version.0

View file

@ -12,8 +12,8 @@ use futures::future::Either;
use futures::{pin_mut, FutureExt, StreamExt, TryFutureExt};
use pubgrub::error::PubGrubError;
use pubgrub::range::Range;
use pubgrub::solver::{DependencyConstraints, Incompatibility, State};
use pubgrub::type_aliases::SelectedDependencies;
use pubgrub::solver::{Incompatibility, State};
use pubgrub::type_aliases::{DependencyConstraints, SelectedDependencies};
use tokio::select;
use tracing::{debug, trace};
use waitmap::WaitMap;
@ -190,8 +190,9 @@ impl<'a> Resolver<'a> {
}
.into());
}
if let Some((dependent, _)) =
constraints.iter().find(|(_, r)| r == &&Range::none())
if let Some((dependent, _)) = constraints
.iter()
.find(|(_, r)| r == &&Range::<PubGrubVersion>::empty())
{
return Err(PubGrubError::DependencyOnTheEmptySet {
package: package.clone(),
@ -378,7 +379,8 @@ impl<'a> Resolver<'a> {
) -> Result<Dependencies, ResolveError> {
match package {
PubGrubPackage::Root => {
let mut constraints = DependencyConstraints::default();
let mut constraints =
DependencyConstraints::<PubGrubPackage, Range<PubGrubVersion>>::default();
// Add the root requirements.
for (package, version) in
@ -420,7 +422,9 @@ impl<'a> Resolver<'a> {
let entry = self.cache.versions.wait(&file.hashes.sha256).await.unwrap();
let metadata = entry.value();
let mut constraints = DependencyConstraints::default();
let mut constraints =
DependencyConstraints::<PubGrubPackage, Range<PubGrubVersion>>::default();
for (package, version) in
iter_requirements(metadata.requires_dist.iter(), extra.as_ref(), self.markers)
{
@ -465,7 +469,7 @@ impl<'a> Resolver<'a> {
}
constraints.insert(
PubGrubPackage::Package(package_name.clone(), None),
Range::exact(version.clone()),
Range::singleton(version.clone()),
);
}
@ -559,13 +563,13 @@ impl Default for SolverCache {
}
/// 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)]
enum Dependencies {
/// Package dependencies are unavailable.
Unknown,
/// Container for all available package versions.
Known(DependencyConstraints<PubGrubPackage, PubGrubVersion>),
Known(DependencyConstraints<PubGrubPackage, Range<PubGrubVersion>>),
}
#[derive(Debug)]

View file

@ -48,7 +48,7 @@ The gist of it is:
#### Added
- Links to code items in the code documenation.
- Links to code items in the code documentation.
- New `"serde"` feature that allows serializing some library types, useful for making simple reproducible bug reports.
- New variants for `error::PubGrubError` which are `DependencyOnTheEmptySet`,
`SelfDependency`, `ErrorChoosingPackageVersion` and `ErrorInShouldCancel`.

View file

@ -23,12 +23,14 @@ include = ["Cargo.toml", "LICENSE", "README.md", "src/**", "tests/**", "examples
thiserror = "1.0"
rustc-hash = "1.1.0"
serde = { version = "1.0", features = ["derive"], optional = true }
log = "0.4.14" # for debug logs in tests
[dev-dependencies]
proptest = "0.10.1"
ron = "0.6"
varisat = "0.2.2"
criterion = "0.3"
env_logger = "0.9.0"
[[bench]]
name = "large_case"

View file

@ -5,16 +5,19 @@ extern crate criterion;
use self::criterion::*;
use pubgrub::package::Package;
use pubgrub::range::Range;
use pubgrub::solver::{resolve, OfflineDependencyProvider};
use pubgrub::version::{NumberVersion, SemanticVersion, Version};
use pubgrub::version::{NumberVersion, SemanticVersion};
use pubgrub::version_set::VersionSet;
use serde::de::Deserialize;
use std::hash::Hash;
fn bench<'a, P: Package + Deserialize<'a>, V: Version + Hash + Deserialize<'a>>(
fn bench<'a, P: Package + Deserialize<'a>, VS: VersionSet + Deserialize<'a>>(
b: &mut Bencher,
case: &'a str,
) {
let dependency_provider: OfflineDependencyProvider<P, V> = ron::de::from_str(&case).unwrap();
) where
<VS as VersionSet>::V: Deserialize<'a>,
{
let dependency_provider: OfflineDependencyProvider<P, VS> = ron::de::from_str(&case).unwrap();
b.iter(|| {
for p in dependency_provider.packages() {
@ -35,11 +38,11 @@ fn bench_nested(c: &mut Criterion) {
let data = std::fs::read_to_string(&case).unwrap();
if name.ends_with("u16_NumberVersion.ron") {
group.bench_function(name, |b| {
bench::<u16, NumberVersion>(b, &data);
bench::<u16, Range<NumberVersion>>(b, &data);
});
} else if name.ends_with("str_SemanticVersion.ron") {
group.bench_function(name, |b| {
bench::<&str, SemanticVersion>(b, &data);
bench::<&str, Range<SemanticVersion>>(b, &data);
});
}
}

View file

@ -6,51 +6,53 @@ use pubgrub::report::{DefaultStringReporter, Reporter};
use pubgrub::solver::{resolve, OfflineDependencyProvider};
use pubgrub::version::SemanticVersion;
type SemVS = Range<SemanticVersion>;
// 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(
"root", (1, 0, 0),
vec![("foo", Range::between((1, 0, 0), (2, 0, 0)))],
[("foo", Range::from_range_bounds((1, 0, 0)..(2, 0, 0)))],
);
#[rustfmt::skip]
// foo 1.0.0 depends on a ^1.0.0 and b ^1.0.0
dependency_provider.add_dependencies(
"foo", (1, 0, 0),
vec![
("a", Range::between((1, 0, 0), (2, 0, 0))),
("b", Range::between((1, 0, 0), (2, 0, 0))),
[
("a", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))),
("b", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))),
],
);
#[rustfmt::skip]
// foo 1.1.0 depends on x ^1.0.0 and y ^1.0.0
dependency_provider.add_dependencies(
"foo", (1, 1, 0),
vec![
("x", Range::between((1, 0, 0), (2, 0, 0))),
("y", Range::between((1, 0, 0), (2, 0, 0))),
[
("x", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))),
("y", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))),
],
);
#[rustfmt::skip]
// a 1.0.0 depends on b ^2.0.0
dependency_provider.add_dependencies(
"a", (1, 0, 0),
vec![("b", Range::between((2, 0, 0), (3, 0, 0)))],
[("b", Range::from_range_bounds((2, 0, 0)..(3, 0, 0)))],
);
// b 1.0.0 and 2.0.0 have no dependencies.
dependency_provider.add_dependencies("b", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("b", (2, 0, 0), vec![]);
dependency_provider.add_dependencies("b", (1, 0, 0), []);
dependency_provider.add_dependencies("b", (2, 0, 0), []);
#[rustfmt::skip]
// x 1.0.0 depends on y ^2.0.0.
dependency_provider.add_dependencies(
"x", (1, 0, 0),
vec![("y", Range::between((2, 0, 0), (3, 0, 0)))],
[("y", Range::from_range_bounds((2, 0, 0)..(3, 0, 0)))],
);
// y 1.0.0 and 2.0.0 have no dependencies.
dependency_provider.add_dependencies("y", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("y", (2, 0, 0), vec![]);
dependency_provider.add_dependencies("y", (1, 0, 0), []);
dependency_provider.add_dependencies("y", (2, 0, 0), []);
// Run the algorithm.
match resolve(&dependency_provider, "root", (1, 0, 0)) {

View file

@ -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<NumberVersion>;
// An example implementing caching dependency provider that will
// store queried dependencies in memory and check them before querying more from remote.
struct CachingDependencyProvider<P: Package, V: Version, DP: DependencyProvider<P, V>> {
struct CachingDependencyProvider<P: Package, VS: VersionSet, DP: DependencyProvider<P, VS>> {
remote_dependencies: DP,
cached_dependencies: RefCell<OfflineDependencyProvider<P, V>>,
cached_dependencies: RefCell<OfflineDependencyProvider<P, VS>>,
}
impl<P: Package, V: Version, DP: DependencyProvider<P, V>> CachingDependencyProvider<P, V, DP> {
impl<P: Package, VS: VersionSet, DP: DependencyProvider<P, VS>>
CachingDependencyProvider<P, VS, DP>
{
pub fn new(remote_dependencies_provider: DP) -> Self {
CachingDependencyProvider {
remote_dependencies: remote_dependencies_provider,
@ -24,13 +29,13 @@ impl<P: Package, V: Version, DP: DependencyProvider<P, V>> CachingDependencyProv
}
}
impl<P: Package, V: Version, DP: DependencyProvider<P, V>> DependencyProvider<P, V>
for CachingDependencyProvider<P, V, DP>
impl<P: Package, VS: VersionSet, DP: DependencyProvider<P, VS>> DependencyProvider<P, VS>
for CachingDependencyProvider<P, VS, DP>
{
fn choose_package_version<T: std::borrow::Borrow<P>, U: std::borrow::Borrow<Range<V>>>(
fn choose_package_version<T: std::borrow::Borrow<P>, U: std::borrow::Borrow<VS>>(
&self,
packages: impl Iterator<Item = (T, U)>,
) -> Result<(T, Option<V>), Box<dyn Error>> {
) -> Result<(T, Option<VS::V>), Box<dyn Error + Send + Sync>> {
self.remote_dependencies.choose_package_version(packages)
}
@ -38,8 +43,8 @@ impl<P: Package, V: Version, DP: DependencyProvider<P, V>> DependencyProvider<P,
fn get_dependencies(
&self,
package: &P,
version: &V,
) -> Result<Dependencies<P, V>, Box<dyn Error>> {
version: &VS::V,
) -> Result<Dependencies<P, VS>, Box<dyn Error + Send + Sync>> {
let mut cache = self.cached_dependencies.borrow_mut();
match cache.get_dependencies(package, version) {
Ok(Dependencies::Unknown) => {
@ -49,7 +54,7 @@ impl<P: Package, V: Version, DP: DependencyProvider<P, V>> DependencyProvider<P,
cache.add_dependencies(
package.clone(),
version.clone(),
dependencies.clone().into_iter(),
dependencies.clone(),
);
Ok(Dependencies::Known(dependencies))
}
@ -65,7 +70,7 @@ impl<P: Package, V: Version, DP: DependencyProvider<P, V>> DependencyProvider<P,
fn main() {
// Simulating remote provider locally.
let mut remote_dependencies_provider = OfflineDependencyProvider::<&str, NumberVersion>::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());

View file

@ -4,19 +4,21 @@ use pubgrub::range::Range;
use pubgrub::solver::{resolve, OfflineDependencyProvider};
use pubgrub::version::NumberVersion;
type NumVS = Range<NumberVersion>;
// `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())],
"root", 1, [("menu", Range::full()), ("icons", Range::full())],
);
dependency_provider.add_dependencies("menu", 1, vec![("dropdown", Range::any())]);
dependency_provider.add_dependencies("dropdown", 1, vec![("icons", Range::any())]);
dependency_provider.add_dependencies("icons", 1, vec![]);
dependency_provider.add_dependencies("menu", 1, [("dropdown", Range::full())]);
dependency_provider.add_dependencies("dropdown", 1, [("icons", Range::full())]);
dependency_provider.add_dependencies("icons", 1, []);
// Run the algorithm.
let solution = resolve(&dependency_provider, "root", 1);

View file

@ -6,6 +6,8 @@ use pubgrub::report::{DefaultStringReporter, Reporter};
use pubgrub::solver::{resolve, OfflineDependencyProvider};
use pubgrub::version::SemanticVersion;
type SemVS = Range<SemanticVersion>;
// `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,59 +17,59 @@ 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()),
("icons", Range::exact((1, 0, 0))),
("intl", Range::exact((5, 0, 0))),
dependency_provider.add_dependencies("root", (1, 0, 0), [
("menu", Range::full()),
("icons", Range::singleton((1, 0, 0))),
("intl", Range::singleton((5, 0, 0))),
]);
// Dependencies of the menu lib.
dependency_provider.add_dependencies("menu", (1, 0, 0), vec![
("dropdown", Range::strictly_lower_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 0, 0), [
("dropdown", Range::from_range_bounds(..(2, 0, 0))),
]);
dependency_provider.add_dependencies("menu", (1, 1, 0), vec![
("dropdown", Range::higher_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 1, 0), [
("dropdown", Range::from_range_bounds((2, 0, 0)..)),
]);
dependency_provider.add_dependencies("menu", (1, 2, 0), vec![
("dropdown", Range::higher_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 2, 0), [
("dropdown", Range::from_range_bounds((2, 0, 0)..)),
]);
dependency_provider.add_dependencies("menu", (1, 3, 0), vec![
("dropdown", Range::higher_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 3, 0), [
("dropdown", Range::from_range_bounds((2, 0, 0)..)),
]);
dependency_provider.add_dependencies("menu", (1, 4, 0), vec![
("dropdown", Range::higher_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 4, 0), [
("dropdown", Range::from_range_bounds((2, 0, 0)..)),
]);
dependency_provider.add_dependencies("menu", (1, 5, 0), vec![
("dropdown", Range::higher_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 5, 0), [
("dropdown", Range::from_range_bounds((2, 0, 0)..)),
]);
// Dependencies of the dropdown lib.
dependency_provider.add_dependencies("dropdown", (1, 8, 0), vec![
("intl", Range::exact((3, 0, 0))),
dependency_provider.add_dependencies("dropdown", (1, 8, 0), [
("intl", Range::singleton((3, 0, 0))),
]);
dependency_provider.add_dependencies("dropdown", (2, 0, 0), vec![
("icons", Range::exact((2, 0, 0))),
dependency_provider.add_dependencies("dropdown", (2, 0, 0), [
("icons", Range::singleton((2, 0, 0))),
]);
dependency_provider.add_dependencies("dropdown", (2, 1, 0), vec![
("icons", Range::exact((2, 0, 0))),
dependency_provider.add_dependencies("dropdown", (2, 1, 0), [
("icons", Range::singleton((2, 0, 0))),
]);
dependency_provider.add_dependencies("dropdown", (2, 2, 0), vec![
("icons", Range::exact((2, 0, 0))),
dependency_provider.add_dependencies("dropdown", (2, 2, 0), [
("icons", Range::singleton((2, 0, 0))),
]);
dependency_provider.add_dependencies("dropdown", (2, 3, 0), vec![
("icons", Range::exact((2, 0, 0))),
dependency_provider.add_dependencies("dropdown", (2, 3, 0), [
("icons", Range::singleton((2, 0, 0))),
]);
// Icons have no dependencies.
dependency_provider.add_dependencies("icons", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("icons", (2, 0, 0), vec![]);
dependency_provider.add_dependencies("icons", (1, 0, 0), []);
dependency_provider.add_dependencies("icons", (2, 0, 0), []);
// Intl have no dependencies.
dependency_provider.add_dependencies("intl", (3, 0, 0), vec![]);
dependency_provider.add_dependencies("intl", (4, 0, 0), vec![]);
dependency_provider.add_dependencies("intl", (5, 0, 0), vec![]);
dependency_provider.add_dependencies("intl", (3, 0, 0), []);
dependency_provider.add_dependencies("intl", (4, 0, 0), []);
dependency_provider.add_dependencies("intl", (5, 0, 0), []);
// Run the algorithm.
match resolve(&dependency_provider, "root", (1, 0, 0)) {

View file

@ -6,6 +6,8 @@ use pubgrub::report::{DefaultStringReporter, Reporter};
use pubgrub::solver::{resolve, OfflineDependencyProvider};
use pubgrub::version::SemanticVersion;
type SemVS = Range<SemanticVersion>;
// `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,51 +16,51 @@ 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()),
("icons", Range::exact((1, 0, 0))),
dependency_provider.add_dependencies("root", (1, 0, 0), [
("menu", Range::full()),
("icons", Range::singleton((1, 0, 0))),
]);
// Dependencies of the menu lib.
dependency_provider.add_dependencies("menu", (1, 0, 0), vec![
("dropdown", Range::strictly_lower_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 0, 0), [
("dropdown", Range::from_range_bounds(..(2, 0, 0))),
]);
dependency_provider.add_dependencies("menu", (1, 1, 0), vec![
("dropdown", Range::higher_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 1, 0), [
("dropdown", Range::from_range_bounds((2, 0, 0)..)),
]);
dependency_provider.add_dependencies("menu", (1, 2, 0), vec![
("dropdown", Range::higher_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 2, 0), [
("dropdown", Range::from_range_bounds((2, 0, 0)..)),
]);
dependency_provider.add_dependencies("menu", (1, 3, 0), vec![
("dropdown", Range::higher_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 3, 0), [
("dropdown", Range::from_range_bounds((2, 0, 0)..)),
]);
dependency_provider.add_dependencies("menu", (1, 4, 0), vec![
("dropdown", Range::higher_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 4, 0), [
("dropdown", Range::from_range_bounds((2, 0, 0)..)),
]);
dependency_provider.add_dependencies("menu", (1, 5, 0), vec![
("dropdown", Range::higher_than((2, 0, 0))),
dependency_provider.add_dependencies("menu", (1, 5, 0), [
("dropdown", Range::from_range_bounds((2, 0, 0)..)),
]);
// Dependencies of the dropdown lib.
dependency_provider.add_dependencies("dropdown", (1, 8, 0), vec![]);
dependency_provider.add_dependencies("dropdown", (2, 0, 0), vec![
("icons", Range::exact((2, 0, 0))),
dependency_provider.add_dependencies("dropdown", (1, 8, 0), []);
dependency_provider.add_dependencies("dropdown", (2, 0, 0), [
("icons", Range::singleton((2, 0, 0))),
]);
dependency_provider.add_dependencies("dropdown", (2, 1, 0), vec![
("icons", Range::exact((2, 0, 0))),
dependency_provider.add_dependencies("dropdown", (2, 1, 0), [
("icons", Range::singleton((2, 0, 0))),
]);
dependency_provider.add_dependencies("dropdown", (2, 2, 0), vec![
("icons", Range::exact((2, 0, 0))),
dependency_provider.add_dependencies("dropdown", (2, 2, 0), [
("icons", Range::singleton((2, 0, 0))),
]);
dependency_provider.add_dependencies("dropdown", (2, 3, 0), vec![
("icons", Range::exact((2, 0, 0))),
dependency_provider.add_dependencies("dropdown", (2, 3, 0), [
("icons", Range::singleton((2, 0, 0))),
]);
// Icons has no dependency.
dependency_provider.add_dependencies("icons", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("icons", (2, 0, 0), vec![]);
dependency_provider.add_dependencies("icons", (1, 0, 0), []);
dependency_provider.add_dependencies("icons", (2, 0, 0), []);
// Run the algorithm.
match resolve(&dependency_provider, "root", (1, 0, 0)) {

View file

@ -6,33 +6,35 @@ use pubgrub::report::{DefaultStringReporter, Reporter};
use pubgrub::solver::{resolve, OfflineDependencyProvider};
use pubgrub::version::SemanticVersion;
type SemVS = Range<SemanticVersion>;
// 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(
"root", (1, 0, 0),
vec![
("foo", Range::between((1, 0, 0), (2, 0, 0))),
("baz", Range::between((1, 0, 0), (2, 0, 0))),
[
("foo", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))),
("baz", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))),
],
);
#[rustfmt::skip]
// foo 1.0.0 depends on bar ^2.0.0
dependency_provider.add_dependencies(
"foo", (1, 0, 0),
vec![("bar", Range::between((2, 0, 0), (3, 0, 0)))],
[("bar", Range::from_range_bounds((2, 0, 0)..(3, 0, 0)))],
);
#[rustfmt::skip]
// bar 2.0.0 depends on baz ^3.0.0
dependency_provider.add_dependencies(
"bar", (2, 0, 0),
vec![("baz", Range::between((3, 0, 0), (4, 0, 0)))],
[("baz", Range::from_range_bounds((3, 0, 0)..(4, 0, 0)))],
);
// baz 1.0.0 and 3.0.0 have no dependencies
dependency_provider.add_dependencies("baz", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("baz", (3, 0, 0), vec![]);
dependency_provider.add_dependencies("baz", (1, 0, 0), []);
dependency_provider.add_dependencies("baz", (3, 0, 0), []);
// Run the algorithm.
match resolve(&dependency_provider, "root", (1, 0, 0)) {

28
vendor/pubgrub/release.md vendored Normal file
View file

@ -0,0 +1,28 @@
# Creation of a new release
This is taking the 0.2.1 release as an example.
## GitHub stuff
- Checkout the prep-v0.2.1 branch
- Update the release date in the changelog and push to the PR.
- Squash merge the PR to the dev branch
- Check that the merged PR is passing the tests on the dev branch
- Pull the updated dev locally
- Switch to the release branch
- Merge locally dev into release in fast-forward mode, we want to keep the history of commits and the merge point.
- `git tag -a v0.2.1 -m "v0.2.1: mostly perf improvements"`
- (Optional) cryptographically sign the tag
- On GitHub, edit the branch protection setting for release: uncheck include admin, and save
- Push release to github: git push --follow-tags
- Reset the release branch protection to include admins
- On GitHub, create a release from that tag.
## Crates.io stuff
- `cargo publish --dry-run`
- `cargo publish`
## Community stuff
Talk about the awesome new features of the new release online.

View file

@ -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<P: Package, V: Version> {
pub enum PubGrubError<P: Package, VS: VersionSet> {
/// There is no solution for this set of dependencies.
#[error("No solution")]
NoSolution(DerivationTree<P, V>),
NoSolution(DerivationTree<P, VS>),
/// Error arising when the implementer of
/// [DependencyProvider](crate::solver::DependencyProvider)
@ -24,7 +24,7 @@ pub enum PubGrubError<P: Package, V: Version> {
/// 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<dyn std::error::Error + Send + Sync>,
@ -40,7 +40,7 @@ pub enum PubGrubError<P: Package, V: Version> {
/// 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<P: Package, V: Version> {
/// 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

View file

@ -55,7 +55,7 @@ impl<T> Id<T> {
}
fn from(n: u32) -> Self {
Self {
raw: n as u32,
raw: n,
_ty: PhantomData,
}
}

View file

@ -15,28 +15,27 @@ 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::version::Version;
use crate::type_aliases::{DependencyConstraints, Map};
use crate::version_set::VersionSet;
/// Current state of the PubGrub algorithm.
#[derive(Clone)]
pub struct State<P: Package, V: Version> {
pub struct State<P: Package, VS: VersionSet> {
root_package: P,
root_version: V,
root_version: VS::V,
incompatibilities: Map<P, Vec<IncompId<P, V>>>,
incompatibilities: Map<P, Vec<IncompId<P, VS>>>,
/// 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<IncompId<P, V>>,
contradicted_incompatibilities: rustc_hash::FxHashSet<IncompId<P, VS>>,
/// Partial solution.
/// TODO: remove pub.
pub partial_solution: PartialSolution<P, V>,
pub partial_solution: PartialSolution<P, VS>,
/// The store is the reference storage for all incompatibilities.
pub incompatibility_store: Arena<Incompatibility<P, V>>,
pub incompatibility_store: Arena<Incompatibility<P, VS>>,
/// This is a stack of work to be done in `unit_propagation`.
/// It can definitely be a local variable to that method, but
@ -44,9 +43,9 @@ pub struct State<P: Package, V: Version> {
unit_propagation_buffer: SmallVec<P>,
}
impl<P: Package, V: Version> State<P, V> {
impl<P: Package, VS: VersionSet> State<P, VS> {
/// 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(),
@ -66,7 +65,7 @@ impl<P: Package, V: Version> State<P, V> {
}
/// Add an incompatibility to the state.
pub fn add_incompatibility(&mut self, incompat: Incompatibility<P, V>) {
pub fn add_incompatibility(&mut self, incompat: Incompatibility<P, VS>) {
let id = self.incompatibility_store.alloc(incompat);
self.merge_incompatibility(id);
}
@ -75,9 +74,9 @@ impl<P: Package, V: Version> State<P, V> {
pub fn add_incompatibility_from_dependencies(
&mut self,
package: P,
version: V,
deps: &DependencyConstraints<P, V>,
) -> std::ops::Range<IncompId<P, V>> {
version: VS::V,
deps: &DependencyConstraints<P, VS>,
) -> std::ops::Range<IncompId<P, VS>> {
// Create incompatibilities and allocate them in the store.
let new_incompats_id_range = self
.incompatibility_store
@ -92,13 +91,13 @@ impl<P: Package, V: Version> State<P, V> {
}
/// Check if an incompatibility is terminal.
pub fn is_terminal(&self, incompatibility: &Incompatibility<P, V>) -> bool {
pub fn is_terminal(&self, incompatibility: &Incompatibility<P, VS>) -> bool {
incompatibility.is_terminal(&self.root_package, &self.root_version)
}
/// Unit propagation is the core mechanism of the solving algorithm.
/// CF <https://github.com/dart-lang/pub/blob/master/doc/solver.md#unit-propagation>
pub fn unit_propagation(&mut self, package: P) -> Result<(), PubGrubError<P, V>> {
pub fn unit_propagation(&mut self, package: P) -> Result<(), PubGrubError<P, VS>> {
self.unit_propagation_buffer.clear();
self.unit_propagation_buffer.push(package);
while let Some(current_package) = self.unit_propagation_buffer.pop() {
@ -115,6 +114,10 @@ impl<P: Package, V: Version> State<P, V> {
// If the partial solution satisfies the incompatibility
// we must perform conflict resolution.
Relation::Satisfied => {
log::info!(
"Start conflict resolution because incompat satisfied:\n {}",
current_incompat
);
conflict_id = Some(incompat_id);
break;
}
@ -158,8 +161,8 @@ impl<P: Package, V: Version> State<P, V> {
/// CF <https://github.com/dart-lang/pub/blob/master/doc/solver.md#unit-propagation>
fn conflict_resolution(
&mut self,
incompatibility: IncompId<P, V>,
) -> Result<(P, IncompId<P, V>), PubGrubError<P, V>> {
incompatibility: IncompId<P, VS>,
) -> Result<(P, IncompId<P, VS>), PubGrubError<P, VS>> {
let mut current_incompat_id = incompatibility;
let mut current_incompat_changed = false;
loop {
@ -183,6 +186,7 @@ impl<P: Package, V: Version> State<P, V> {
current_incompat_changed,
previous_satisfier_level,
);
log::info!("backtrack to {:?}", previous_satisfier_level);
return Ok((package, current_incompat_id));
}
SameDecisionLevels { satisfier_cause } => {
@ -192,6 +196,7 @@ impl<P: Package, V: Version> State<P, V> {
&package,
&self.incompatibility_store,
);
log::info!("prior cause: {}", prior_cause);
current_incompat_id = self.incompatibility_store.alloc(prior_cause);
current_incompat_changed = true;
}
@ -203,7 +208,7 @@ impl<P: Package, V: Version> State<P, V> {
/// Backtracking.
fn backtrack(
&mut self,
incompat: IncompId<P, V>,
incompat: IncompId<P, VS>,
incompat_changed: bool,
decision_level: DecisionLevel,
) {
@ -234,7 +239,7 @@ impl<P: Package, V: Version> State<P, V> {
/// Here we do the simple stupid thing of just growing the Vec.
/// It may not be trivial since those incompatibilities
/// may already have derived others.
fn merge_incompatibility(&mut self, id: IncompId<P, V>) {
fn merge_incompatibility(&mut self, id: IncompId<P, VS>) {
for (pkg, _term) in self.incompatibility_store[id].iter() {
self.incompatibilities
.entry(pkg.clone())
@ -245,12 +250,12 @@ impl<P: Package, V: Version> State<P, V> {
// Error reporting #########################################################
fn build_derivation_tree(&self, incompat: IncompId<P, V>) -> DerivationTree<P, V> {
fn build_derivation_tree(&self, incompat: IncompId<P, VS>) -> DerivationTree<P, VS> {
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<P, V>) -> Set<IncompId<P, V>> {
fn find_shared_ids(&self, incompat: IncompId<P, VS>) -> Set<IncompId<P, VS>> {
let mut all_ids = Set::new();
let mut shared_ids = Set::new();
let mut stack = vec![incompat];

View file

@ -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<P: Package, V: Version> {
package_terms: SmallMap<P, Term<V>>,
kind: Kind<P, V>,
pub struct Incompatibility<P: Package, VS: VersionSet> {
package_terms: SmallMap<P, Term<VS>>,
kind: Kind<P, VS>,
}
/// Type alias of unique identifiers for incompatibilities.
pub(crate) type IncompId<P, V> = Id<Incompatibility<P, V>>;
pub type IncompId<P, VS> = Id<Incompatibility<P, VS>>;
#[derive(Debug, Clone)]
enum Kind<P: Package, V: Version> {
enum Kind<P: Package, VS: VersionSet> {
/// 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<V>),
NoVersions(P, VS),
/// Dependencies of the package are unavailable for versions in that range.
UnavailableDependencies(P, Range<V>),
UnavailableDependencies(P, VS),
/// Incompatibility coming from the dependencies of a given package.
FromDependencyOf(P, Range<V>, P, Range<V>),
FromDependencyOf(P, VS, P, VS),
/// Derived from two causes. Stores cause ids.
DerivedFrom(IncompId<P, V>, IncompId<P, V>),
DerivedFrom(IncompId<P, VS>, IncompId<P, VS>),
}
/// A Relation describes how a set of terms can be compared to an incompatibility.
@ -69,52 +68,52 @@ pub enum Relation<P: Package> {
Inconclusive,
}
impl<P: Package, V: Version> Incompatibility<P, V> {
impl<P: Package, VS: VersionSet> Incompatibility<P, VS> {
/// 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<V>) -> Self {
let range = match &term {
/// that a given set does not contain any version.
pub fn no_versions(package: P, term: Term<VS>) -> 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<V>)) -> 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,24 +144,24 @@ impl<P: Package, V: Version> Incompatibility<P, V> {
/// 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 {
false
} else {
let (package, term) = self.package_terms.iter().next().unwrap();
(package == root_package) && term.contains(&root_version)
(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<V>> {
pub fn get(&self, package: &P) -> Option<&Term<VS>> {
self.package_terms.get(package)
}
/// Iterate over packages.
pub fn iter(&self) -> impl Iterator<Item = (&P, &Term<V>)> {
pub fn iter(&self) -> impl Iterator<Item = (&P, &Term<VS>)> {
self.package_terms.iter()
}
@ -181,7 +180,7 @@ impl<P: Package, V: Version> Incompatibility<P, V> {
self_id: Id<Self>,
shared_ids: &Set<Id<Self>>,
store: &Arena<Self>,
) -> DerivationTree<P, V> {
) -> DerivationTree<P, VS> {
match &store[self_id].kind {
Kind::DerivedFrom(id1, id2) => {
let cause1 = Self::build_derivation_tree(*id1, shared_ids, store);
@ -197,30 +196,30 @@ impl<P: Package, V: Version> Incompatibility<P, V> {
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<P, V> {
impl<'a, P: Package, VS: VersionSet + 'a> Incompatibility<P, VS> {
/// CF definition of Relation enum.
pub fn relation(&self, terms: impl Fn(&P) -> Option<&'a Term<V>>) -> Relation<P> {
pub fn relation(&self, terms: impl Fn(&P) -> Option<&'a Term<VS>>) -> Relation<P> {
let mut relation = Relation::Satisfied;
for (package, incompat_term) in self.package_terms.iter() {
match terms(package).map(|term| incompat_term.relation_with(&term)) {
match terms(package).map(|term| incompat_term.relation_with(term)) {
Some(term::Relation::Satisfied) => {}
Some(term::Relation::Contradicted) => {
return Relation::Contradicted(package.clone());
@ -243,7 +242,7 @@ impl<'a, P: Package, V: Version + 'a> Incompatibility<P, V> {
}
}
impl<P: Package, V: Version> fmt::Display for Incompatibility<P, V> {
impl<P: Package, VS: VersionSet> fmt::Display for Incompatibility<P, VS> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
@ -258,6 +257,7 @@ impl<P: Package, V: Version> fmt::Display for Incompatibility<P, V> {
#[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::*;
@ -276,12 +276,12 @@ pub mod tests {
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())
kind: Kind::UnavailableDependencies("0", Range::full())
});
let i2 = store.alloc(Incompatibility {
package_terms: SmallMap::Two([("p2", t2), ("p3", t3.clone())]),
kind: Kind::UnavailableDependencies("0", Range::any())
kind: Kind::UnavailableDependencies("0", Range::full())
});
let mut i3 = Map::default();

View file

@ -2,9 +2,9 @@
//! Non exposed modules.
pub(crate) mod arena;
pub(crate) mod core;
pub(crate) mod incompatibility;
pub(crate) mod partial_solution;
pub(crate) mod small_map;
pub(crate) mod small_vec;
pub mod arena;
pub mod core;
pub mod incompatibility;
pub mod partial_solution;
pub mod small_map;
pub mod small_vec;

View file

@ -1,16 +1,17 @@
// 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).
//! where terms are regrouped by package in a [Map].
use std::fmt::Display;
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 +27,100 @@ impl DecisionLevel {
/// The partial solution contains all package assignments,
/// organized by package and historically ordered.
#[derive(Clone, Debug)]
pub struct PartialSolution<P: Package, V: Version> {
pub struct PartialSolution<P: Package, VS: VersionSet> {
next_global_index: u32,
current_decision_level: DecisionLevel,
package_assignments: Map<P, PackageAssignments<P, V>>,
package_assignments: Map<P, PackageAssignments<P, VS>>,
}
impl<P: Package, VS: VersionSet> Display for PartialSolution<P, VS> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut assignments: Vec<_> = self
.package_assignments
.iter()
.map(|(p, pa)| format!("{}: {}", p, pa))
.collect();
assignments.sort();
write!(
f,
"next_global_index: {}\ncurrent_decision_level: {:?}\npackage_assignements:\n{}",
self.next_global_index,
self.current_decision_level,
assignments.join("\t\n")
)
}
}
/// 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<P: Package, V: Version> {
struct PackageAssignments<P: Package, VS: VersionSet> {
smallest_decision_level: DecisionLevel,
highest_decision_level: DecisionLevel,
dated_derivations: SmallVec<DatedDerivation<P, V>>,
assignments_intersection: AssignmentsIntersection<V>,
dated_derivations: SmallVec<DatedDerivation<P, VS>>,
assignments_intersection: AssignmentsIntersection<VS>,
}
impl<P: Package, VS: VersionSet> Display for PackageAssignments<P, VS> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let derivations: Vec<_> = self
.dated_derivations
.iter()
.map(|dd| dd.to_string())
.collect();
write!(
f,
"decision range: {:?}..{:?}\nderivations:\n {}\n,assignments_intersection: {}",
self.smallest_decision_level,
self.highest_decision_level,
derivations.join("\n "),
self.assignments_intersection
)
}
}
#[derive(Clone, Debug)]
pub(crate) struct DatedDerivation<P: Package, V: Version> {
pub struct DatedDerivation<P: Package, VS: VersionSet> {
global_index: u32,
decision_level: DecisionLevel,
cause: IncompId<P, V>,
cause: IncompId<P, VS>,
}
impl<P: Package, VS: VersionSet> Display for DatedDerivation<P, VS> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}, cause: {:?}", self.decision_level, self.cause)
}
}
#[derive(Clone, Debug)]
enum AssignmentsIntersection<V: Version> {
Decision((u32, V, Term<V>)),
Derivations(Term<V>),
enum AssignmentsIntersection<VS: VersionSet> {
Decision((u32, VS::V, Term<VS>)),
Derivations(Term<VS>),
}
impl<VS: VersionSet> Display for AssignmentsIntersection<VS> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Decision((lvl, version, _)) => {
write!(f, "Decision: level {}, v = {}", lvl, version)
}
Self::Derivations(term) => write!(f, "Derivations term: {}", term),
}
}
}
#[derive(Clone, Debug)]
pub enum SatisfierSearch<P: Package, V: Version> {
pub enum SatisfierSearch<P: Package, VS: VersionSet> {
DifferentDecisionLevels {
previous_satisfier_level: DecisionLevel,
},
SameDecisionLevels {
satisfier_cause: IncompId<P, V>,
satisfier_cause: IncompId<P, VS>,
},
}
impl<P: Package, V: Version> PartialSolution<P, V> {
impl<P: Package, VS: VersionSet> PartialSolution<P, VS> {
/// Initialize an empty PartialSolution.
pub fn empty() -> Self {
Self {
@ -77,7 +131,7 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
}
/// 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 +164,8 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
pub fn add_derivation(
&mut self,
package: P,
cause: IncompId<P, V>,
store: &Arena<Incompatibility<P, V>>,
cause: IncompId<P, VS>,
store: &Arena<Incompatibility<P, VS>>,
) {
use std::collections::hash_map::Entry;
let term = store[cause].get(&package).unwrap().negate();
@ -153,7 +207,7 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
/// selected version (no "decision")
/// and if it contains at least one positive derivation term
/// in the partial solution.
pub fn potential_packages(&self) -> Option<impl Iterator<Item = (&P, &Range<V>)>> {
pub fn potential_packages(&self) -> Option<impl Iterator<Item = (&P, &VS)>> {
let mut iter = self
.package_assignments
.iter()
@ -169,7 +223,7 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
/// 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<SelectedDependencies<P, V>> {
pub fn extract_solution(&self) -> Option<SelectedDependencies<P, VS::V>> {
let mut solution = Map::default();
for (p, pa) in &self.package_assignments {
match &pa.assignments_intersection {
@ -190,7 +244,7 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
pub fn backtrack(
&mut self,
decision_level: DecisionLevel,
store: &Arena<Incompatibility<P, V>>,
store: &Arena<Incompatibility<P, VS>>,
) {
self.current_decision_level = decision_level;
self.package_assignments.retain(|p, pa| {
@ -223,7 +277,7 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
pa.dated_derivations
.iter()
.fold(Term::any(), |acc, dated_derivation| {
let term = store[dated_derivation.cause].get(&p).unwrap().negate();
let term = store[dated_derivation.cause].get(p).unwrap().negate();
acc.intersection(&term)
}),
);
@ -240,12 +294,12 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
pub fn add_version(
&mut self,
package: P,
version: V,
new_incompatibilities: std::ops::Range<IncompId<P, V>>,
store: &Arena<Incompatibility<P, V>>,
version: VS::V,
new_incompatibilities: std::ops::Range<IncompId<P, VS>>,
store: &Arena<Incompatibility<P, VS>>,
) {
let exact = Term::exact(version.clone());
let not_satisfied = |incompat: &Incompatibility<P, V>| {
let not_satisfied = |incompat: &Incompatibility<P, VS>| {
incompat.relation(|p| {
if p == &package {
Some(&exact)
@ -258,17 +312,24 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
// Check none of the dependencies (new_incompatibilities)
// would create a conflict (be satisfied).
if store[new_incompatibilities].iter().all(not_satisfied) {
log::info!("add_decision: {} @ {}", package, version);
self.add_decision(package, version);
} else {
log::info!(
"not adding {} @ {} because of its dependencies",
package,
version
);
}
}
/// Check if the terms in the partial solution satisfy the incompatibility.
pub fn relation(&self, incompat: &Incompatibility<P, V>) -> Relation<P> {
pub fn relation(&self, incompat: &Incompatibility<P, VS>) -> Relation<P> {
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<V>> {
pub fn term_intersection_for_package(&self, package: &P) -> Option<&Term<VS>> {
self.package_assignments
.get(package)
.map(|pa| pa.assignments_intersection.term())
@ -277,9 +338,9 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
/// Figure out if the satisfier and previous satisfier are of different decision levels.
pub fn satisfier_search(
&self,
incompat: &Incompatibility<P, V>,
store: &Arena<Incompatibility<P, V>>,
) -> (P, SatisfierSearch<P, V>) {
incompat: &Incompatibility<P, VS>,
store: &Arena<Incompatibility<P, VS>>,
) -> (P, SatisfierSearch<P, VS>) {
let satisfied_map = Self::find_satisfier(incompat, &self.package_assignments, store);
let (satisfier_package, &(satisfier_index, _, satisfier_decision_level)) = satisfied_map
.iter()
@ -318,9 +379,9 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
/// 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<P, V>,
package_assignments: &Map<P, PackageAssignments<P, V>>,
store: &Arena<Incompatibility<P, V>>,
incompat: &Incompatibility<P, VS>,
package_assignments: &Map<P, PackageAssignments<P, VS>>,
store: &Arena<Incompatibility<P, VS>>,
) -> SmallMap<P, (usize, u32, DecisionLevel)> {
let mut satisfied = SmallMap::Empty;
for (package, incompat_term) in incompat.iter() {
@ -337,11 +398,11 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
/// such that incompatibility is satisfied by the partial solution up to
/// and including that assignment plus satisfier.
fn find_previous_satisfier(
incompat: &Incompatibility<P, V>,
incompat: &Incompatibility<P, VS>,
satisfier_package: &P,
mut satisfied_map: SmallMap<P, (usize, u32, DecisionLevel)>,
package_assignments: &Map<P, PackageAssignments<P, V>>,
store: &Arena<Incompatibility<P, V>>,
package_assignments: &Map<P, PackageAssignments<P, VS>>,
store: &Arena<Incompatibility<P, VS>>,
) -> DecisionLevel {
// First, let's retrieve the previous derivations and the initial accum_term.
let satisfier_pa = package_assignments.get(satisfier_package).unwrap();
@ -375,13 +436,13 @@ impl<P: Package, V: Version> PartialSolution<P, V> {
}
}
impl<P: Package, V: Version> PackageAssignments<P, V> {
impl<P: Package, VS: VersionSet> PackageAssignments<P, VS> {
fn satisfier(
&self,
package: &P,
incompat_term: &Term<V>,
start_term: Term<V>,
store: &Arena<Incompatibility<P, V>>,
incompat_term: &Term<VS>,
start_term: Term<VS>,
store: &Arena<Incompatibility<P, VS>>,
) -> (usize, u32, DecisionLevel) {
// Term where we accumulate intersections until incompat_term is satisfied.
let mut accum_term = start_term;
@ -407,15 +468,24 @@ impl<P: Package, V: Version> PackageAssignments<P, V> {
self.highest_decision_level,
),
AssignmentsIntersection::Derivations(_) => {
panic!("This must be a decision")
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<V: Version> AssignmentsIntersection<V> {
impl<VS: VersionSet> AssignmentsIntersection<VS> {
/// Returns the term intersection of all assignments (decision included).
fn term(&self) -> &Term<V> {
fn term(&self) -> &Term<VS> {
match self {
Self::Decision((_, _, term)) => term,
Self::Derivations(term) => term,
@ -429,7 +499,7 @@ impl<V: Version> AssignmentsIntersection<V> {
fn potential_package_filter<'a, P: Package>(
&'a self,
package: &'a P,
) -> Option<(&'a P, &'a Range<V>)> {
) -> Option<(&'a P, &'a VS)> {
match self {
Self::Decision(_) => None,
Self::Derivations(term_intersection) => {

View file

@ -2,7 +2,7 @@ use crate::type_aliases::Map;
use std::hash::Hash;
#[derive(Debug, Clone)]
pub(crate) enum SmallMap<K, V> {
pub enum SmallMap<K, V> {
Empty,
One([(K, V); 1]),
Two([(K, V); 2]),
@ -10,7 +10,7 @@ pub(crate) enum SmallMap<K, V> {
}
impl<K: PartialEq + Eq + Hash, V> SmallMap<K, V> {
pub(crate) fn get(&self, key: &K) -> Option<&V> {
pub fn get(&self, key: &K) -> Option<&V> {
match self {
Self::Empty => None,
Self::One([(k, v)]) if k == key => Some(v),
@ -22,7 +22,7 @@ impl<K: PartialEq + Eq + Hash, V> SmallMap<K, V> {
}
}
pub(crate) fn get_mut(&mut self, key: &K) -> Option<&mut V> {
pub fn get_mut(&mut self, key: &K) -> Option<&mut V> {
match self {
Self::Empty => None,
Self::One([(k, v)]) if k == key => Some(v),
@ -34,7 +34,7 @@ impl<K: PartialEq + Eq + Hash, V> SmallMap<K, V> {
}
}
pub(crate) fn remove(&mut self, key: &K) -> Option<V> {
pub fn remove(&mut self, key: &K) -> Option<V> {
let out;
*self = match std::mem::take(self) {
Self::Empty => {
@ -70,7 +70,7 @@ impl<K: PartialEq + Eq + Hash, V> SmallMap<K, V> {
out
}
pub(crate) fn insert(&mut self, key: K, value: V) {
pub fn insert(&mut self, key: K, value: V) {
*self = match std::mem::take(self) {
Self::Empty => Self::One([(key, value)]),
Self::One([(k, v)]) => {
@ -108,7 +108,7 @@ impl<K: Clone + PartialEq + Eq + Hash, V: Clone> SmallMap<K, V> {
/// apply the provided function to both values.
/// If the result is None, remove that key from the merged map,
/// otherwise add the content of the Some(_).
pub(crate) fn merge<'a>(
pub fn merge<'a>(
&'a mut self,
map_2: impl Iterator<Item = (&'a K, &'a V)>,
f: impl Fn(&V, &V) -> Option<V>,
@ -136,7 +136,7 @@ impl<K, V> Default for SmallMap<K, V> {
}
impl<K, V> SmallMap<K, V> {
pub(crate) fn len(&self) -> usize {
pub fn len(&self) -> usize {
match self {
Self::Empty => 0,
Self::One(_) => 1,
@ -147,7 +147,7 @@ impl<K, V> SmallMap<K, V> {
}
impl<K: Eq + Hash + Clone, V: Clone> SmallMap<K, V> {
pub(crate) fn as_map(&self) -> Map<K, V> {
pub fn as_map(&self) -> Map<K, V> {
match self {
Self::Empty => Map::default(),
Self::One([(k, v)]) => {
@ -184,7 +184,7 @@ impl<'a, K: 'a, V: 'a> Iterator for IterSmallMap<'a, K, V> {
}
impl<K, V> SmallMap<K, V> {
pub(crate) fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
pub fn iter(&self) -> impl Iterator<Item = (&K, &V)> {
match self {
Self::Empty => IterSmallMap::Inline([].iter()),
Self::One(data) => IterSmallMap::Inline(data.iter()),

View file

@ -1,4 +1,5 @@
use std::fmt;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
#[derive(Clone)]
@ -108,6 +109,13 @@ impl<T: fmt::Debug> fmt::Debug for SmallVec<T> {
}
}
impl<T: Hash> Hash for SmallVec<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.len().hash(state);
Hash::hash_slice(self.as_slice(), state);
}
}
#[cfg(feature = "serde")]
impl<T: serde::Serialize> serde::Serialize for SmallVec<T> {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
@ -118,13 +126,70 @@ impl<T: serde::Serialize> serde::Serialize for SmallVec<T> {
#[cfg(feature = "serde")]
impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for SmallVec<T> {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let items: Vec<T> = serde::Deserialize::deserialize(d)?;
let mut v = Self::empty();
for item in items {
v.push(item);
struct SmallVecVisitor<T> {
marker: std::marker::PhantomData<T>,
}
impl<'de, T> serde::de::Visitor<'de> for SmallVecVisitor<T>
where
T: serde::Deserialize<'de>,
{
type Value = SmallVec<T>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a sequence")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut values = SmallVec::empty();
while let Some(value) = seq.next_element()? {
values.push(value);
}
Ok(values)
}
}
let visitor = SmallVecVisitor {
marker: Default::default(),
};
d.deserialize_seq(visitor)
}
}
impl<T> IntoIterator for SmallVec<T> {
type Item = T;
type IntoIter = SmallVecIntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
match self {
SmallVec::Empty => SmallVecIntoIter::Empty,
SmallVec::One(a) => SmallVecIntoIter::One(IntoIterator::into_iter(a)),
SmallVec::Two(a) => SmallVecIntoIter::Two(IntoIterator::into_iter(a)),
SmallVec::Flexible(v) => SmallVecIntoIter::Flexible(IntoIterator::into_iter(v)),
}
}
}
pub enum SmallVecIntoIter<T> {
Empty,
One(<[T; 1] as IntoIterator>::IntoIter),
Two(<[T; 2] as IntoIterator>::IntoIter),
Flexible(<Vec<T> as IntoIterator>::IntoIter),
}
impl<T> Iterator for SmallVecIntoIter<T> {
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
match self {
SmallVecIntoIter::Empty => None,
SmallVecIntoIter::One(it) => it.next(),
SmallVecIntoIter::Two(it) => it.next(),
SmallVecIntoIter::Flexible(it) => it.next(),
}
Ok(v)
}
}
@ -137,11 +202,11 @@ pub mod tests {
proptest! {
#[test]
fn push_and_pop(comands: Vec<Option<u8>>) {
fn push_and_pop(commands: Vec<Option<u8>>) {
let mut v = vec![];
let mut sv = SmallVec::Empty;
for comand in comands {
match comand {
for command in commands {
match command {
Some(i) => {
v.push(i);
sv.push(i);

View file

@ -1,5 +1,7 @@
// SPDX-License-Identifier: MPL-2.0
#![allow(clippy::all, unreachable_pub)]
//! PubGrub version solving algorithm.
//!
//! Version solving consists in efficiently finding a set of packages and versions
@ -49,15 +51,17 @@
//! # use pubgrub::solver::{OfflineDependencyProvider, resolve};
//! # use pubgrub::version::NumberVersion;
//! # use pubgrub::range::Range;
//! #
//! let mut dependency_provider = OfflineDependencyProvider::<&str, NumberVersion>::new();
//!
//! type NumVS = Range<NumberVersion>;
//!
//! let mut dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new();
//!
//! dependency_provider.add_dependencies(
//! "root", 1, vec![("menu", Range::any()), ("icons", Range::any())],
//! "root", 1, [("menu", Range::full()), ("icons", Range::full())],
//! );
//! dependency_provider.add_dependencies("menu", 1, vec![("dropdown", Range::any())]);
//! dependency_provider.add_dependencies("dropdown", 1, vec![("icons", Range::any())]);
//! dependency_provider.add_dependencies("icons", 1, vec![]);
//! dependency_provider.add_dependencies("menu", 1, [("dropdown", Range::full())]);
//! dependency_provider.add_dependencies("dropdown", 1, [("icons", Range::full())]);
//! dependency_provider.add_dependencies("icons", 1, []);
//!
//! // Run the algorithm.
//! let solution = resolve(&dependency_provider, "root", 1).unwrap();
@ -84,8 +88,10 @@
//! #
//! # struct MyDependencyProvider;
//! #
//! impl DependencyProvider<String, SemanticVersion> for MyDependencyProvider {
//! fn choose_package_version<T: Borrow<String>, U: Borrow<Range<SemanticVersion>>>(&self,packages: impl Iterator<Item=(T, U)>) -> Result<(T, Option<SemanticVersion>), Box<dyn Error>> {
//! type SemVS = Range<SemanticVersion>;
//!
//! impl DependencyProvider<String, SemVS> for MyDependencyProvider {
//! fn choose_package_version<T: Borrow<String>, U: Borrow<SemVS>>(&self,packages: impl Iterator<Item=(T, U)>) -> Result<(T, Option<SemanticVersion>), Box<dyn Error + Send + Sync>> {
//! unimplemented!()
//! }
//!
@ -93,7 +99,7 @@
//! &self,
//! package: &String,
//! version: &SemanticVersion,
//! ) -> Result<Dependencies<String, SemanticVersion>, Box<dyn Error>> {
//! ) -> Result<Dependencies<String, SemVS>, Box<dyn Error + Send + Sync>> {
//! unimplemented!()
//! }
//! }
@ -153,13 +159,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<P: Package, V: Version> {
//! pub trait Reporter<P: Package, VS: VersionSet> {
//! type Output;
//!
//! fn report(derivation_tree: &DerivationTree<P, V>) -> Self::Output;
//! fn report(derivation_tree: &DerivationTree<P, VS>) -> Self::Output;
//! }
//! ```
//! Implementing a [Reporter](crate::report::Reporter) may involve a lot of heuristics
@ -173,8 +179,11 @@
//! # use pubgrub::report::{DefaultStringReporter, Reporter};
//! # use pubgrub::error::PubGrubError;
//! # use pubgrub::version::NumberVersion;
//! # use pubgrub::range::Range;
//! #
//! # let dependency_provider = OfflineDependencyProvider::<&str, NumberVersion>::new();
//! # type NumVS = Range<NumberVersion>;
//! #
//! # let dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new();
//! # let root_package = "root";
//! # let root_version = 1;
//! #
@ -217,5 +226,6 @@ pub mod solver;
pub mod term;
pub mod type_aliases;
pub mod version;
pub mod version_set;
mod internal;

View file

@ -2,16 +2,16 @@
//! Trait for identifying packages.
//! Automatically implemented for traits implementing
//! [Clone] + [Eq] + [Hash] + [Debug] + [Display](std::fmt::Display).
//! [Clone] + [Eq] + [Hash] + [Debug] + [Display].
use std::fmt::{Debug, Display};
use std::hash::Hash;
/// Trait for identifying packages.
/// Automatically implemented for types already implementing
/// [Clone] + [Eq] + [Hash] + [Debug] + [Display](std::fmt::Display).
/// [Clone] + [Eq] + [Hash] + [Debug] + [Display].
pub trait Package: Clone + Eq + Hash + Debug + Display {}
/// Automatically implement the Package trait for any type
/// that already implement [Clone] + [Eq] + [Hash] + [Debug] + [Display](std::fmt::Display).
/// that already implement [Clone] + [Eq] + [Hash] + [Debug] + [Display].
impl<T: Clone + Eq + Hash + Debug + Display> Package for T {}

View file

@ -7,287 +7,425 @@
//! 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
//! - [empty()](Range::empty): the empty set
//! - [full()](Range::full): the set of all possible versions
//! - [singleton(v)](Range::singleton): the set containing only the version v
//! - [higher_than(v)](Range::higher_than): the set defined by `v <= versions`
//! - [strictly_higher_than(v)](Range::strictly_higher_than): the set defined by `v < versions`
//! - [lower_than(v)](Range::lower_than): the set defined by `versions <= v`
//! - [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`
//!
//! Ranges can be created from any type that implements [`Ord`] + [`Clone`].
//!
//! In order to advance the solver front, comparisons of versions sets are necessary in the algorithm.
//! To do those comparisons between two sets S1 and S2 we use the mathematical property that S1 ⊂ S2 if and only if S1 ∩ S2 == S1.
//! We can thus compute an intersection and evaluate an equality to answer if S1 is a subset of S2.
//! But this means that the implementation of equality must be correct semantically.
//! In practice, if equality is derived automatically, this means sets must have unique representations.
//!
//! By migrating from a custom representation for discrete sets in v0.2
//! to a generic bounded representation for continuous sets in v0.3
//! we are potentially breaking that assumption in two ways:
//!
//! 1. Minimal and maximal `Unbounded` values can be replaced by their equivalent if it exists.
//! 2. Simplifying adjacent bounds of discrete sets cannot be detected and automated in the generic intersection code.
//!
//! An example for each can be given when `T` is `u32`.
//! First, we can have both segments `S1 = (Unbounded, Included(42u32))` and `S2 = (Included(0), Included(42u32))`
//! that represent the same segment but are structurally different.
//! Thus, a derived equality check would answer `false` to `S1 == S2` while it's true.
//!
//! Second both segments `S1 = (Included(1), Included(5))` and `S2 = (Included(1), Included(3)) + (Included(4), Included(5))` are equal.
//! But without asking the user to provide a `bump` function for discrete sets,
//! the algorithm is not able tell that the space between the right `Included(3)` bound and the left `Included(4)` bound is empty.
//! Thus the algorithm is not able to reduce S2 to its canonical S1 form while computing sets operations like intersections in the generic code.
//!
//! This is likely to lead to user facing theoretically correct but practically nonsensical ranges,
//! like (Unbounded, Excluded(0)) or (Excluded(6), Excluded(7)).
//! In general nonsensical inputs often lead to hard to track bugs.
//! But as far as we can tell this should work in practice.
//! So for now this crate only provides an implementation for continuous ranges.
//! With the v0.3 api the user could choose to bring back the discrete implementation from v0.2, as documented in the guide.
//! If doing so regularly fixes bugs seen by users, we will bring it back into the core library.
//! If we do not see practical bugs, or we get a formal proof that the code cannot lead to error states, then we may remove this warning.
use std::cmp::Ordering;
use std::fmt;
use crate::{internal::small_vec::SmallVec, version_set::VersionSet};
use std::ops::RangeBounds;
use std::{
fmt::{Debug, Display, Formatter},
ops::Bound::{self, Excluded, Included, Unbounded},
};
use crate::internal::small_vec::SmallVec;
use crate::version::Version;
/// A Range is a set of versions.
#[derive(Debug, Clone, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
/// A Range represents multiple intervals of a continuous range of monotone increasing
/// values.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct Range<V: Version> {
pub struct Range<V> {
segments: SmallVec<Interval<V>>,
}
type Interval<V> = (V, Option<V>);
type Interval<V> = (Bound<V>, Bound<V>);
// Range building blocks.
impl<V: Version> Range<V> {
impl<V> Range<V> {
/// Empty set of versions.
pub fn none() -> Self {
pub fn empty() -> 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<V>) -> Self {
let v = v.into();
/// Set of all possible versions
pub fn full() -> Self {
Self {
segments: SmallVec::one((v.clone(), Some(v.bump()))),
segments: SmallVec::one((Unbounded, Unbounded)),
}
}
/// Set of all versions higher or equal to some version.
/// Set of all versions higher or equal to some version
pub fn higher_than(v: impl Into<V>) -> Self {
Self {
segments: SmallVec::one((v.into(), None)),
segments: SmallVec::one((Included(v.into()), Unbounded)),
}
}
/// Set of all versions strictly lower than some version.
/// Set of all versions higher to some version
pub fn strictly_higher_than(v: impl Into<V>) -> Self {
Self {
segments: SmallVec::one((Excluded(v.into()), Unbounded)),
}
}
/// Set of all versions lower to some version
pub fn strictly_lower_than(v: impl Into<V>) -> Self {
let v = v.into();
if v == V::lowest() {
Self::none()
} else {
Self {
segments: SmallVec::one((V::lowest(), Some(v))),
}
Self {
segments: SmallVec::one((Unbounded, Excluded(v.into()))),
}
}
/// Set of all versions comprised between two given versions.
/// The lower bound is included and the higher bound excluded.
/// `v1 <= v < v2`.
/// Set of all versions lower or equal to some version
pub fn lower_than(v: impl Into<V>) -> Self {
Self {
segments: SmallVec::one((Unbounded, Included(v.into()))),
}
}
/// Set of versions greater or equal to `v1` but less than `v2`.
pub fn between(v1: impl Into<V>, v2: impl Into<V>) -> Self {
let v1 = v1.into();
let v2 = v2.into();
if v1 < v2 {
Self {
segments: SmallVec::one((v1, Some(v2))),
}
} else {
Self::none()
Self {
segments: SmallVec::one((Included(v1.into()), Excluded(v2.into()))),
}
}
}
// Set operations.
impl<V: Version> Range<V> {
// Negate ##################################################################
impl<V: Clone> Range<V> {
/// Set containing exactly one version
pub fn singleton(v: impl Into<V>) -> Self {
let v = v.into();
Self {
segments: SmallVec::one((Included(v.clone()), Included(v))),
}
}
/// Compute the complement set of versions.
pub fn negate(&self) -> Self {
/// Returns the complement of this Range.
pub fn complement(&self) -> Self {
match self.segments.first() {
None => Self::any(), // Complement of ∅ is *
// Complement of ∅ is ∞
None => Self::full(),
// Complement of ∞ is ∅
Some((Unbounded, Unbounded)) => Self::empty(),
// 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())
}
}
Some((Included(v), Unbounded)) => Self::strictly_lower_than(v.clone()),
Some((Excluded(v), Unbounded)) => Self::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)
}
Some((Unbounded, Included(v))) => {
Self::negate_segments(Excluded(v.clone()), &self.segments[1..])
}
Some((Unbounded, Excluded(v))) => {
Self::negate_segments(Included(v.clone()), &self.segments[1..])
}
Some((Included(_), Included(_)))
| Some((Included(_), Excluded(_)))
| Some((Excluded(_), Included(_)))
| Some((Excluded(_), Excluded(_))) => Self::negate_segments(Unbounded, &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<V>]) -> Range<V> {
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();
fn negate_segments(start: Bound<V>, segments: &[Interval<V>]) -> Self {
let mut complement_segments: SmallVec<Interval<V>> = SmallVec::empty();
let mut start = start;
for (v1, v2) in segments {
complement_segments.push((
start,
match v1 {
Included(v) => Excluded(v.clone()),
Excluded(v) => Included(v.clone()),
Unbounded => unreachable!(),
},
));
start = match v2 {
Included(v) => Excluded(v.clone()),
Excluded(v) => Included(v.clone()),
Unbounded => Unbounded,
}
}
if let Some(last) = start {
complement_segments.push((last, None));
if !matches!(start, Unbounded) {
complement_segments.push((start, Unbounded));
}
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<V: Version> Range<V> {
/// 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;
}
}
impl<V: Ord> Range<V> {
/// Convert to something that can be used with
/// [BTreeMap::range](std::collections::BTreeMap::range).
/// All versions contained in self, will be in the output,
/// but there may be versions in the output that are not contained in self.
/// Returns None if the range is empty.
pub fn bounding_range(&self) -> Option<(Bound<&V>, Bound<&V>)> {
self.segments.first().map(|(start, _)| {
let end = self
.segments
.last()
.expect("if there is a first element, there must be a last element");
(bound_as_ref(start), bound_as_ref(&end.1))
})
}
/// Returns true if the this Range contains the specified value.
pub fn contains(&self, v: &V) -> bool {
for segment in self.segments.iter() {
if match segment {
(Unbounded, Unbounded) => true,
(Unbounded, Included(end)) => v <= end,
(Unbounded, Excluded(end)) => v < end,
(Included(start), Unbounded) => v >= start,
(Included(start), Included(end)) => v >= start && v <= end,
(Included(start), Excluded(end)) => v >= start && v < end,
(Excluded(start), Unbounded) => v > start,
(Excluded(start), Included(end)) => v > start && v <= end,
(Excluded(start), Excluded(end)) => v > start && v < end,
} {
return true;
}
}
false
}
/// Return the lowest version in the range (if there is one).
pub fn lowest_version(&self) -> Option<V> {
self.segments.first().map(|(start, _)| start).cloned()
/// Construct a simple range from anything that impls [RangeBounds] like `v1..v2`.
pub fn from_range_bounds<R, IV>(bounds: R) -> Self
where
R: RangeBounds<IV>,
IV: Clone + Into<V>,
{
let start = match bounds.start_bound() {
Included(v) => Included(v.clone().into()),
Excluded(v) => Excluded(v.clone().into()),
Unbounded => Unbounded,
};
let end = match bounds.end_bound() {
Included(v) => Included(v.clone().into()),
Excluded(v) => Excluded(v.clone().into()),
Unbounded => Unbounded,
};
if valid_segment(&start, &end) {
Self {
segments: SmallVec::one((start, end)),
}
} else {
Self::empty()
}
}
fn check_invariants(self) -> Self {
if cfg!(debug_assertions) {
for p in self.segments.as_slice().windows(2) {
match (&p[0].1, &p[1].0) {
(Included(l_end), Included(r_start)) => assert!(l_end < r_start),
(Included(l_end), Excluded(r_start)) => assert!(l_end < r_start),
(Excluded(l_end), Included(r_start)) => assert!(l_end < r_start),
(Excluded(l_end), Excluded(r_start)) => assert!(l_end <= r_start),
(_, Unbounded) => panic!(),
(Unbounded, _) => panic!(),
}
}
for (s, e) in self.segments.iter() {
assert!(valid_segment(s, e));
}
}
self
}
}
/// Implementation of [`Bound::as_ref`] which is currently marked as unstable.
fn bound_as_ref<V>(bound: &Bound<V>) -> Bound<&V> {
match bound {
Included(v) => Included(v),
Excluded(v) => Excluded(v),
Unbounded => Unbounded,
}
}
fn valid_segment<T: PartialOrd>(start: &Bound<T>, end: &Bound<T>) -> bool {
match (start, end) {
(Included(s), Included(e)) => s <= e,
(Included(s), Excluded(e)) => s < e,
(Excluded(s), Included(e)) => s < e,
(Excluded(s), Excluded(e)) => s < e,
(Unbounded, _) | (_, Unbounded) => true,
}
}
impl<V: Ord + Clone> Range<V> {
/// Computes the union of this `Range` and another.
pub fn union(&self, other: &Self) -> Self {
self.complement()
.intersection(&other.complement())
.complement()
.check_invariants()
}
/// Computes the intersection of two sets of versions.
pub fn intersection(&self, other: &Self) -> Self {
let mut segments: SmallVec<Interval<V>> = SmallVec::empty();
let mut left_iter = self.segments.iter().peekable();
let mut right_iter = other.segments.iter().peekable();
while let (Some((left_start, left_end)), Some((right_start, right_end))) =
(left_iter.peek(), right_iter.peek())
{
let start = match (left_start, right_start) {
(Included(l), Included(r)) => Included(std::cmp::max(l, r)),
(Excluded(l), Excluded(r)) => Excluded(std::cmp::max(l, r)),
(Included(i), Excluded(e)) | (Excluded(e), Included(i)) if i <= e => Excluded(e),
(Included(i), Excluded(e)) | (Excluded(e), Included(i)) if e < i => Included(i),
(s, Unbounded) | (Unbounded, s) => bound_as_ref(s),
_ => unreachable!(),
}
.cloned();
let end = match (left_end, right_end) {
(Included(l), Included(r)) => Included(std::cmp::min(l, r)),
(Excluded(l), Excluded(r)) => Excluded(std::cmp::min(l, r)),
(Included(i), Excluded(e)) | (Excluded(e), Included(i)) if i >= e => Excluded(e),
(Included(i), Excluded(e)) | (Excluded(e), Included(i)) if e > i => Included(i),
(s, Unbounded) | (Unbounded, s) => bound_as_ref(s),
_ => unreachable!(),
}
.cloned();
left_iter.next_if(|(_, e)| e == &end);
right_iter.next_if(|(_, e)| e == &end);
if valid_segment(&start, &end) {
segments.push((start, end))
}
}
Self { segments }.check_invariants()
}
}
impl<T: Debug + Display + Clone + Eq + Ord> VersionSet for Range<T> {
type V = T;
fn empty() -> Self {
Range::empty()
}
fn singleton(v: Self::V) -> Self {
Range::singleton(v)
}
fn complement(&self) -> Self {
Range::complement(self)
}
fn intersection(&self, other: &Self) -> Self {
Range::intersection(self, other)
}
fn contains(&self, v: &Self::V) -> bool {
Range::contains(self, v)
}
fn full() -> Self {
Range::full()
}
fn union(&self, other: &Self) -> Self {
Range::union(self, other)
}
}
// REPORT ######################################################################
impl<V: Version> fmt::Display for Range<V> {
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(" "))
impl<V: Display + Eq> Display for Range<V> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
if self.segments.is_empty() {
write!(f, "")?;
} else {
for (idx, segment) in self.segments.iter().enumerate() {
if idx > 0 {
write!(f, ", ")?;
}
match segment {
(Unbounded, Unbounded) => write!(f, "*")?,
(Unbounded, Included(v)) => write!(f, "<={v}")?,
(Unbounded, Excluded(v)) => write!(f, "<{v}")?,
(Included(v), Unbounded) => write!(f, ">={v}")?,
(Included(v), Included(b)) => {
if v == b {
write!(f, "{v}")?
} else {
write!(f, ">={v},<={b}")?
}
}
(Included(v), Excluded(b)) => write!(f, ">={v}, <{b}")?,
(Excluded(v), Unbounded) => write!(f, ">{v}")?,
(Excluded(v), Included(b)) => write!(f, ">{v}, <={b}")?,
(Excluded(v), Excluded(b)) => write!(f, ">{v}, <{b}")?,
};
}
}
Ok(())
}
}
fn interval_to_string<V: Version>((start, maybe_end): &Interval<V>) -> String {
match maybe_end {
Some(end) => format!("[ {}, {} [", start, end),
None => format!("[ {}, ∞ [", start),
// SERIALIZATION ###############################################################
#[cfg(feature = "serde")]
impl<'de, V: serde::Deserialize<'de>> serde::Deserialize<'de> for Range<V> {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
// This enables conversion from the "old" discrete implementation of `Range` to the new
// bounded one.
//
// Serialization is always performed in the new format.
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum EitherInterval<V> {
B(Bound<V>, Bound<V>),
D(V, Option<V>),
}
let bounds: SmallVec<EitherInterval<V>> = serde::Deserialize::deserialize(deserializer)?;
let mut segments = SmallVec::Empty;
for i in bounds {
match i {
EitherInterval::B(l, r) => segments.push((l, r)),
EitherInterval::D(l, Some(r)) => segments.push((Included(l), Excluded(r))),
EitherInterval::D(l, None) => segments.push((Included(l), Unbounded)),
}
}
Ok(Range { segments })
}
}
@ -297,28 +435,74 @@ fn interval_to_string<V: Version>((start, maybe_end): &Interval<V>) -> String {
pub mod tests {
use proptest::prelude::*;
use crate::version::NumberVersion;
use super::*;
pub fn strategy() -> impl Strategy<Value = Range<NumberVersion>> {
prop::collection::vec(any::<u32>(), 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 }
})
/// Generate version sets from a random vector of deltas between bounds.
/// Each bound is randomly inclusive or exclusive.
pub fn strategy() -> impl Strategy<Value = Range<u32>> {
(
any::<bool>(),
prop::collection::vec(any::<(u32, bool)>(), 1..10),
)
.prop_map(|(start_unbounded, deltas)| {
let mut start = if start_unbounded {
Some(Unbounded)
} else {
None
};
let mut largest: u32 = 0;
let mut last_bound_was_inclusive = false;
let mut segments = SmallVec::Empty;
for (delta, inclusive) in deltas {
// Add the offset to the current bound
largest = match largest.checked_add(delta) {
Some(s) => s,
None => {
// Skip this offset, if it would result in a too large bound.
continue;
}
};
let current_bound = if inclusive {
Included(largest)
} else {
Excluded(largest)
};
// If we already have a start bound, the next offset defines the complete range.
// If we don't have a start bound, we have to generate one.
if let Some(start_bound) = start.take() {
// If the delta from the start bound is 0, the only authorized configuration is
// Included(x), Included(x)
if delta == 0 && !(matches!(start_bound, Included(_)) && inclusive) {
start = Some(start_bound);
continue;
}
last_bound_was_inclusive = inclusive;
segments.push((start_bound, current_bound));
} else {
// If the delta from the end bound of the last range is 0 and
// any of the last ending or current starting bound is inclusive,
// we skip the delta because they basically overlap.
if delta == 0 && (last_bound_was_inclusive || inclusive) {
continue;
}
start = Some(current_bound);
}
}
// If we still have a start bound, but didn't have enough deltas to complete another
// segment, we add an unbounded upperbound.
if let Some(start_bound) = start {
segments.push((start_bound, Unbounded));
}
return Range { segments }.check_invariants();
})
}
fn version_strat() -> impl Strategy<Value = NumberVersion> {
any::<u32>().prop_map(NumberVersion)
fn version_strat() -> impl Strategy<Value = u32> {
any::<u32>()
}
proptest! {
@ -327,17 +511,17 @@ pub mod tests {
#[test]
fn negate_is_different(range in strategy()) {
assert_ne!(range.negate(), range);
assert_ne!(range.complement(), range);
}
#[test]
fn double_negate_is_identity(range in strategy()) {
assert_eq!(range.negate().negate(), range);
assert_eq!(range.complement().complement(), range);
}
#[test]
fn negate_contains_opposite(range in strategy(), version in version_strat()) {
assert_ne!(range.contains(&version), range.negate().contains(&version));
assert_ne!(range.contains(&version), range.complement().contains(&version));
}
// Testing intersection ----------------------------
@ -349,12 +533,12 @@ pub mod tests {
#[test]
fn intersection_with_any_is_identity(range in strategy()) {
assert_eq!(Range::any().intersection(&range), range);
assert_eq!(Range::full().intersection(&range), range);
}
#[test]
fn intersection_with_none_is_none(range in strategy()) {
assert_eq!(Range::none().intersection(&range), Range::none());
assert_eq!(Range::empty().intersection(&range), Range::empty());
}
#[test]
@ -369,7 +553,7 @@ pub mod tests {
#[test]
fn intesection_of_complements_is_none(range in strategy()) {
assert_eq!(range.negate().intersection(&range), Range::none());
assert_eq!(range.complement().intersection(&range), Range::empty());
}
#[test]
@ -381,7 +565,7 @@ pub mod tests {
#[test]
fn union_of_complements_is_any(range in strategy()) {
assert_eq!(range.negate().union(&range), Range::any());
assert_eq!(range.complement().union(&range), Range::full());
}
#[test]
@ -393,17 +577,37 @@ pub mod tests {
#[test]
fn always_contains_exact(version in version_strat()) {
assert!(Range::exact(version).contains(&version));
assert!(Range::singleton(version).contains(&version));
}
#[test]
fn contains_negation(range in strategy(), version in version_strat()) {
assert_ne!(range.contains(&version), range.negate().contains(&version));
assert_ne!(range.contains(&version), range.complement().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());
assert_eq!(range.contains(&version), range.intersection(&Range::singleton(version)) != Range::empty());
}
#[test]
fn contains_bounding_range(range in strategy(), version in version_strat()) {
if range.contains(&version) {
assert!(range.bounding_range().map(|b| b.contains(&version)).unwrap_or(false));
}
}
#[test]
fn from_range_bounds(range in any::<(Bound<u32>, Bound<u32>)>(), version in version_strat()) {
let rv: Range<_> = Range::from_range_bounds(range);
assert_eq!(range.contains(&version), rv.contains(&version));
}
#[test]
fn from_range_bounds_round_trip(range in any::<(Bound<u32>, Bound<u32>)>()) {
let rv: Range<u32> = Range::from_range_bounds(range);
let rv2: Range<u32> = rv.bounding_range().map(Range::from_range_bounds::<_, u32>).unwrap_or_else(Range::empty);
assert_eq!(rv, rv2);
}
}
}

View file

@ -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<P: Package, V: Version> {
pub trait Reporter<P: Package, VS: VersionSet> {
/// Output type of the report.
type Output;
/// Generate a report from the derivation tree
/// describing the resolution failure.
fn report(derivation_tree: &DerivationTree<P, V>) -> Self::Output;
fn report(derivation_tree: &DerivationTree<P, VS>) -> Self::Output;
}
/// Derivation tree resulting in the impossibility
/// to solve the dependencies of our root package.
#[derive(Debug, Clone)]
pub enum DerivationTree<P: Package, V: Version> {
pub enum DerivationTree<P: Package, VS: VersionSet> {
/// External incompatibility.
External(External<P, V>),
External(External<P, VS>),
/// Incompatibility derived from two others.
Derived(Derived<P, V>),
Derived(Derived<P, VS>),
}
/// Incompatibilities that are not derived from others,
/// they have their own reason.
#[derive(Debug, Clone)]
pub enum External<P: Package, V: Version> {
pub enum External<P: Package, VS: VersionSet> {
/// 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<V>),
/// Dependencies of the package are unavailable for versions in that range.
UnavailableDependencies(P, Range<V>),
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<V>, P, Range<V>),
FromDependencyOf(P, VS, P, VS),
}
/// Incompatibility derived from two others.
#[derive(Debug, Clone)]
pub struct Derived<P: Package, V: Version> {
pub struct Derived<P: Package, VS: VersionSet> {
/// Terms of the incompatibility.
pub terms: Map<P, Term<V>>,
pub terms: Map<P, Term<VS>>,
/// 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<P: Package, V: Version> {
/// and refer to the explanation for the other times.
pub shared_id: Option<usize>,
/// First cause.
pub cause1: Box<DerivationTree<P, V>>,
pub cause1: Box<DerivationTree<P, VS>>,
/// Second cause.
pub cause2: Box<DerivationTree<P, V>>,
pub cause2: Box<DerivationTree<P, VS>>,
}
impl<P: Package, V: Version> DerivationTree<P, V> {
impl<P: Package, VS: VersionSet> DerivationTree<P, VS> {
/// Merge the [NoVersions](External::NoVersions) external incompatibilities
/// with the other one they are matched with
/// in a derived incompatibility.
@ -100,7 +99,7 @@ impl<P: Package, V: Version> DerivationTree<P, V> {
}
}
fn merge_no_versions(self, package: P, range: Range<V>) -> Option<Self> {
fn merge_no_versions(self, package: P, set: VS) -> Option<Self> {
match self {
// TODO: take care of the Derived case.
// Once done, we can remove the Option.
@ -109,19 +108,16 @@ impl<P: Package, V: Version> DerivationTree<P, V> {
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<P: Package, V: Version> DerivationTree<P, V> {
p1,
r1,
p2,
r2.union(&range),
r2.union(&set),
)))
}
}
@ -138,39 +134,39 @@ impl<P: Package, V: Version> DerivationTree<P, V> {
}
}
impl<P: Package, V: Version> fmt::Display for External<P, V> {
impl<P: Package, VS: VersionSet> fmt::Display for External<P, VS> {
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,17 +194,17 @@ impl DefaultStringReporter {
}
}
fn build_recursive<P: Package, V: Version>(&mut self, derived: &Derived<P, V>) {
fn build_recursive<P: Package, VS: VersionSet>(&mut self, derived: &Derived<P, VS>) {
self.build_recursive_helper(derived);
if let Some(id) = derived.shared_id {
if self.shared_with_ref.get(&id) == None {
if self.shared_with_ref.get(&id).is_none() {
self.add_line_ref();
self.shared_with_ref.insert(id, self.ref_count);
}
};
}
fn build_recursive_helper<P: Package, V: Version>(&mut self, current: &Derived<P, V>) {
fn build_recursive_helper<P: Package, VS: VersionSet>(&mut self, current: &Derived<P, VS>) {
match (current.cause1.deref(), current.cause2.deref()) {
(DerivationTree::External(external1), DerivationTree::External(external2)) => {
// Simplest case, we just combine two external incompatibilities.
@ -264,7 +260,7 @@ impl DefaultStringReporter {
// and finally conclude.
(None, None) => {
self.build_recursive(derived1);
if derived1.shared_id != None {
if derived1.shared_id.is_some() {
self.lines.push("".into());
self.build_recursive(current);
} else {
@ -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<P: Package, V: Version>(
fn report_one_each<P: Package, VS: VersionSet>(
&mut self,
derived: &Derived<P, V>,
external: &External<P, V>,
current_terms: &Map<P, Term<V>>,
derived: &Derived<P, VS>,
external: &External<P, VS>,
current_terms: &Map<P, Term<VS>>,
) {
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<P: Package, V: Version>(
fn report_recurse_one_each<P: Package, VS: VersionSet>(
&mut self,
derived: &Derived<P, V>,
external: &External<P, V>,
current_terms: &Map<P, Term<V>>,
derived: &Derived<P, VS>,
external: &External<P, VS>,
current_terms: &Map<P, Term<VS>>,
) {
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<P: Package, V: Version>(
external1: &External<P, V>,
external2: &External<P, V>,
current_terms: &Map<P, Term<V>>,
fn explain_both_external<P: Package, VS: VersionSet>(
external1: &External<P, VS>,
external2: &External<P, VS>,
current_terms: &Map<P, Term<VS>>,
) -> 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<P: Package, V: Version>(
fn explain_both_ref<P: Package, VS: VersionSet>(
ref_id1: usize,
derived1: &Derived<P, V>,
derived1: &Derived<P, VS>,
ref_id2: usize,
derived2: &Derived<P, V>,
current_terms: &Map<P, Term<V>>,
derived2: &Derived<P, VS>,
current_terms: &Map<P, Term<VS>>,
) -> 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<P: Package, V: Version>(
fn explain_ref_and_external<P: Package, VS: VersionSet>(
ref_id: usize,
derived: &Derived<P, V>,
external: &External<P, V>,
current_terms: &Map<P, Term<V>>,
derived: &Derived<P, VS>,
external: &External<P, VS>,
current_terms: &Map<P, Term<VS>>,
) -> 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<P: Package, V: Version>(
external: &External<P, V>,
current_terms: &Map<P, Term<V>>,
fn and_explain_external<P: Package, VS: VersionSet>(
external: &External<P, VS>,
current_terms: &Map<P, Term<VS>>,
) -> String {
format!(
"And because {}, {}.",
@ -406,10 +402,10 @@ impl DefaultStringReporter {
}
/// Add an already explained incompat to the chain of explanations.
fn and_explain_ref<P: Package, V: Version>(
fn and_explain_ref<P: Package, VS: VersionSet>(
ref_id: usize,
derived: &Derived<P, V>,
current_terms: &Map<P, Term<V>>,
derived: &Derived<P, VS>,
current_terms: &Map<P, Term<VS>>,
) -> 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<P: Package, V: Version>(
prior_external: &External<P, V>,
external: &External<P, V>,
current_terms: &Map<P, Term<V>>,
fn and_explain_prior_and_external<P: Package, VS: VersionSet>(
prior_external: &External<P, VS>,
external: &External<P, VS>,
current_terms: &Map<P, Term<VS>>,
) -> 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<P: Package, V: Version>(terms: &Map<P, Term<V>>) -> String {
pub fn string_terms<P: Package, VS: VersionSet>(terms: &Map<P, Term<VS>>) -> String {
let terms_vec: Vec<_> = terms.iter().collect();
match terms_vec.as_slice() {
[] => "version solving failed".into(),
@ -469,10 +465,10 @@ impl DefaultStringReporter {
}
}
impl<P: Package, V: Version> Reporter<P, V> for DefaultStringReporter {
impl<P: Package, VS: VersionSet> Reporter<P, VS> for DefaultStringReporter {
type Output = String;
fn report(derivation_tree: &DerivationTree<P, V>) -> Self::Output {
fn report(derivation_tree: &DerivationTree<P, VS>) -> Self::Output {
match derivation_tree {
DerivationTree::External(external) => external.to_string(),
DerivationTree::Derived(derived) => {

View file

@ -27,8 +27,8 @@
//!
//! 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
//! the [Package] and [Version](crate::version::Version) traits.
//! [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
@ -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<NumberVersion>;
//! #
//! # 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)?;
@ -73,49 +76,56 @@ use crate::error::PubGrubError;
pub use crate::internal::core::State;
pub use crate::internal::incompatibility::Incompatibility;
use crate::package::Package;
use crate::range::Range;
use crate::type_aliases::{Map, SelectedDependencies};
use crate::version::Version;
use crate::type_aliases::{DependencyConstraints, Map, SelectedDependencies};
use crate::version_set::VersionSet;
use log::{debug, info};
/// Main function of the library.
/// Finds a set of packages satisfying dependency bounds for a given package + version pair.
pub fn resolve<P: Package, V: Version>(
dependency_provider: &impl DependencyProvider<P, V>,
pub fn resolve<P: Package, VS: VersionSet>(
dependency_provider: &impl DependencyProvider<P, VS>,
package: P,
version: impl Into<V>,
) -> Result<SelectedDependencies<P, V>, PubGrubError<P, V>> {
version: impl Into<VS::V>,
) -> Result<SelectedDependencies<P, VS::V>, PubGrubError<P, VS>> {
let mut state = State::init(package.clone(), version.into());
let mut added_dependencies: Map<P, Set<V>> = Map::default();
let mut added_dependencies: Map<P, Set<VS::V>> = Map::default();
let mut next = package;
loop {
dependency_provider
.should_cancel()
.map_err(|err| PubGrubError::ErrorInShouldCancel(err))?;
info!("unit_propagation: {}", &next);
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.
debug!(
"Partial solution after unit propagation: {}",
state.partial_solution
);
let Some(potential_packages) = state.partial_solution.potential_packages() else {
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())
.choose_package_version(potential_packages)
.map_err(PubGrubError::ErrorChoosingPackageVersion)?;
info!("DP chose: {} @ {:?}", decision.0, decision.1);
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.");
.ok_or_else(|| {
PubGrubError::Failure("a package was chosen but we don't have a term.".into())
})?;
let v = match decision.1 {
None => {
let inc = Incompatibility::no_versions(next.clone(), term_intersection.clone());
@ -124,108 +134,107 @@ pub fn resolve<P: Package, V: Version>(
}
Some(x) => x,
};
if !term_intersection.contains(&v) {
return Err(PubGrubError::ErrorChoosingPackageVersion(
"choose_package_version picked an incompatible version".into(),
));
}
if added_dependencies
let is_new_dependency = 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 == &&Range::none()) {
return Err(PubGrubError::DependencyOnTheEmptySet {
package: p.clone(),
version: v.clone(),
dependent: dependent.clone(),
});
}
x
}
};
.insert(v.clone());
// 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 {
if !is_new_dependency {
// `dep_incompats` are already in `incompatibilities` so we know there are not satisfied
// terms and can add the decision directly.
info!("add_decision (not first time): {} @ {}", &next, v);
state.partial_solution.add_decision(next.clone(), v);
continue;
}
// Retrieve that package dependencies.
let p = &next;
let dependencies = dependency_provider.get_dependencies(p, &v).map_err(|err| {
PubGrubError::ErrorRetrievingDependencies {
package: p.clone(),
version: v.clone(),
source: err,
}
})?;
let known_dependencies = match dependencies {
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(),
});
}
Dependencies::Known(x) => {
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(), &known_dependencies);
// TODO: I don't think this check can actually happen.
// We might want to put it under #[cfg(debug_assertions)].
let incompatible_self_dependency = state.incompatibility_store[dep_incompats.clone()]
.iter()
.any(|incompat| state.is_terminal(incompat));
if incompatible_self_dependency {
// 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,
);
}
}
/// 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<P: Package, V: Version> {
pub enum Dependencies<P: Package, VS: VersionSet> {
/// Package dependencies are unavailable.
Unknown,
/// Container for all available package versions.
Known(DependencyConstraints<P, V>),
Known(DependencyConstraints<P, VS>),
}
/// 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<P, Range<V>>](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<P, V> = Map<P, Range<V>>;
/// 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<P: Package, V: Version> {
pub trait DependencyProvider<P: Package, VS: VersionSet> {
/// [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,
/// 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
@ -246,18 +255,19 @@ pub trait DependencyProvider<P: Package, V: Version> {
/// 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<T: Borrow<P>, U: Borrow<Range<V>>>(
#[allow(clippy::type_complexity)]
fn choose_package_version<T: Borrow<P>, U: Borrow<VS>>(
&self,
potential_packages: impl Iterator<Item = (T, U)>,
) -> Result<(T, Option<V>), Box<dyn Error + Send + Sync>>;
) -> Result<(T, Option<VS::V>), Box<dyn Error + Send + Sync>>;
/// Retrieves the package dependencies.
/// Return [Dependencies::Unknown] if its dependencies are unknown.
fn get_dependencies(
&self,
package: &P,
version: &V,
) -> Result<Dependencies<P, V>, Box<dyn Error + Send + Sync>>;
version: &VS::V,
) -> Result<Dependencies<P, VS>, Box<dyn Error + Send + Sync>>;
/// This is called fairly regularly during the resolution,
/// if it returns an Err then resolution will be terminated.
@ -276,38 +286,44 @@ pub trait DependencyProvider<P: Package, V: Version> {
/// 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<P: Package, V: Version, T, U, I, F>(
pub fn choose_package_with_fewest_versions<P: Package, VS: VersionSet, T, U, I, F>(
list_available_versions: F,
potential_packages: impl Iterator<Item = (T, U)>,
) -> (T, Option<V>)
) -> (T, Option<VS::V>)
where
T: Borrow<P>,
U: Borrow<Range<V>>,
I: Iterator<Item = V>,
U: Borrow<VS>,
I: Iterator<Item = VS::V>,
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))
.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));
(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(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<P: Package, V: Version> {
dependencies: Map<P, BTreeMap<V, DependencyConstraints<P, V>>>,
pub struct OfflineDependencyProvider<P: Package, VS: VersionSet> {
dependencies: Map<P, BTreeMap<VS::V, DependencyConstraints<P, VS>>>,
}
impl<P: Package, V: Version> OfflineDependencyProvider<P, V> {
impl<P: Package, VS: VersionSet> OfflineDependencyProvider<P, VS> {
/// Creates an empty OfflineDependencyProvider with no dependencies.
pub fn new() -> Self {
Self {
@ -325,10 +341,10 @@ impl<P: Package, V: Version> OfflineDependencyProvider<P, V> {
/// 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<I: IntoIterator<Item = (P, Range<V>)>>(
pub fn add_dependencies<I: IntoIterator<Item = (P, VS)>>(
&mut self,
package: P,
version: impl Into<V>,
version: impl Into<VS::V>,
dependencies: I,
) {
let package_deps = dependencies.into_iter().collect();
@ -348,13 +364,13 @@ impl<P: Package, V: Version> OfflineDependencyProvider<P, V> {
/// 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<impl Iterator<Item = &V>> {
pub fn versions(&self, package: &P) -> Option<impl Iterator<Item = &VS::V>> {
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<DependencyConstraints<P, V>> {
fn dependencies(&self, package: &P, version: &VS::V) -> Option<DependencyConstraints<P, VS>> {
self.dependencies.get(package)?.get(version).cloned()
}
}
@ -363,11 +379,12 @@ impl<P: Package, V: Version> OfflineDependencyProvider<P, V> {
/// 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<P: Package, V: Version> DependencyProvider<P, V> for OfflineDependencyProvider<P, V> {
fn choose_package_version<T: Borrow<P>, U: Borrow<Range<V>>>(
impl<P: Package, VS: VersionSet> DependencyProvider<P, VS> for OfflineDependencyProvider<P, VS> {
#[allow(clippy::type_complexity)]
fn choose_package_version<T: Borrow<P>, U: Borrow<VS>>(
&self,
potential_packages: impl Iterator<Item = (T, U)>,
) -> Result<(T, Option<V>), Box<dyn Error + Send + Sync>> {
) -> Result<(T, Option<VS::V>), Box<dyn Error + Send + Sync>> {
Ok(choose_package_with_fewest_versions(
|p| {
self.dependencies
@ -384,8 +401,8 @@ impl<P: Package, V: Version> DependencyProvider<P, V> for OfflineDependencyProvi
fn get_dependencies(
&self,
package: &P,
version: &V,
) -> Result<Dependencies<P, V>, Box<dyn Error + Send + Sync>> {
version: &VS::V,
) -> Result<Dependencies<P, VS>, Box<dyn Error + Send + Sync>> {
Ok(match self.dependencies(package, version) {
None => Dependencies::Unknown,
Some(dependencies) => Dependencies::Known(dependencies),

View file

@ -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<V: Version> {
pub enum Term<VS: VersionSet> {
/// 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<V>),
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<V>),
Negative(VS),
}
/// Base methods.
impl<V: Version> Term<V> {
impl<VS: VersionSet> Term<VS> {
/// 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<V: Version> Term<V> {
/// 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<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(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<V: Version> Term<V> {
impl<VS: VersionSet> Term<VS> {
/// 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<V>) -> Term<V> {
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<V: Version> Term<V> {
/// 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<V>) -> Term<V> {
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<V>) -> 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<V> {
impl<VS: VersionSet> Term<VS> {
/// 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<V> {
/// 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<V>) -> bool {
fn satisfied_by(&self, terms_intersection: &Self) -> bool {
terms_intersection.subset_of(self)
}
@ -142,13 +141,13 @@ impl<'a, V: 'a + Version> Term<V> {
/// 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<V>) -> 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<V>) -> 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,19 @@ impl<'a, V: 'a + Version> Term<V> {
}
}
impl<V: Version> AsRef<Term<V>> for Term<V> {
fn as_ref(&self) -> &Term<V> {
&self
impl<VS: VersionSet> AsRef<Self> for Term<VS> {
fn as_ref(&self) -> &Self {
self
}
}
// REPORT ######################################################################
impl<V: Version + fmt::Display> fmt::Display for Term<V> {
impl<VS: VersionSet + Display> Display for Term<VS> {
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 +181,10 @@ impl<V: Version + fmt::Display> fmt::Display for Term<V> {
#[cfg(test)]
pub mod tests {
use super::*;
use crate::version::NumberVersion;
use crate::range::Range;
use proptest::prelude::*;
pub fn strategy() -> impl Strategy<Value = Term<NumberVersion>> {
pub fn strategy() -> impl Strategy<Value = Term<Range<u32>>> {
prop_oneof![
crate::range::tests::strategy().prop_map(Term::Positive),
crate::range::tests::strategy().prop_map(Term::Negative),

View file

@ -6,5 +6,12 @@
pub type Map<K, V> = rustc_hash::FxHashMap<K, V>;
/// Concrete dependencies picked by the library during [resolve](crate::solver::resolve)
/// from [DependencyConstraints](crate::solver::DependencyConstraints)
/// from [DependencyConstraints].
pub type SelectedDependencies<P, V> = Map<P, V>;
/// 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 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<P, VS> = Map<P, VS>;

View file

@ -80,6 +80,21 @@ impl From<(u32, u32, u32)> for SemanticVersion {
}
}
// Convert a &(major, minor, patch) into a version.
impl From<&(u32, u32, u32)> for SemanticVersion {
fn from(tuple: &(u32, u32, u32)) -> Self {
let (major, minor, patch) = *tuple;
Self::new(major, minor, patch)
}
}
// Convert an &version into a version.
impl From<&SemanticVersion> for SemanticVersion {
fn from(v: &SemanticVersion) -> Self {
*v
}
}
// Convert a version into a tuple (major, minor, patch).
impl From<SemanticVersion> for (u32, u32, u32) {
fn from(v: SemanticVersion) -> Self {
@ -237,6 +252,20 @@ impl From<u32> for NumberVersion {
}
}
// Convert an &usize into a version.
impl From<&u32> for NumberVersion {
fn from(v: &u32) -> Self {
Self(*v)
}
}
// Convert an &version into a version.
impl From<&NumberVersion> for NumberVersion {
fn from(v: &NumberVersion) -> Self {
*v
}
}
// Convert a version into an usize.
impl From<NumberVersion> for u32 {
fn from(version: NumberVersion) -> Self {

60
vendor/pubgrub/src/version_set.rs vendored Normal file
View file

@ -0,0 +1,60 @@
// SPDX-License-Identifier: MPL-2.0
//! As its name suggests, the [VersionSet] trait describes sets of versions.
//!
//! 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.
//!
//! 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 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.
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: Debug + Display + Clone + Ord;
// 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()
}
}

View file

@ -5,22 +5,37 @@ use pubgrub::solver::{resolve, OfflineDependencyProvider};
use pubgrub::type_aliases::Map;
use pubgrub::version::{NumberVersion, SemanticVersion};
type NumVS = Range<NumberVersion>;
type SemVS = Range<SemanticVersion>;
use log::LevelFilter;
use std::io::Write;
fn init_log() {
let _ = env_logger::builder()
.filter_level(LevelFilter::Trace)
.format(|buf, record| writeln!(buf, "{}", record.args()))
.is_test(true)
.try_init();
}
#[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();
init_log();
let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new();
#[rustfmt::skip]
dependency_provider.add_dependencies(
"root", (1, 0, 0),
vec![("foo", Range::between((1, 0, 0), (2, 0, 0)))],
[("foo", Range::between((1, 0, 0), (2, 0, 0)))],
);
#[rustfmt::skip]
dependency_provider.add_dependencies(
"foo", (1, 0, 0),
vec![("bar", Range::between((1, 0, 0), (2, 0, 0)))],
[("bar", Range::between((1, 0, 0), (2, 0, 0)))],
);
dependency_provider.add_dependencies("bar", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("bar", (2, 0, 0), vec![]);
dependency_provider.add_dependencies("bar", (1, 0, 0), []);
dependency_provider.add_dependencies("bar", (2, 0, 0), []);
// Run the algorithm.
let computed_solution = resolve(&dependency_provider, "root", (1, 0, 0)).unwrap();
@ -38,11 +53,12 @@ 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();
init_log();
let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new();
#[rustfmt::skip]
dependency_provider.add_dependencies(
"root", (1, 0, 0),
vec![
[
("foo", Range::between((1, 0, 0), (2, 0, 0))),
("bar", Range::between((1, 0, 0), (2, 0, 0))),
],
@ -50,12 +66,12 @@ fn avoiding_conflict_during_decision_making() {
#[rustfmt::skip]
dependency_provider.add_dependencies(
"foo", (1, 1, 0),
vec![("bar", Range::between((2, 0, 0), (3, 0, 0)))],
[("bar", Range::between((2, 0, 0), (3, 0, 0)))],
);
dependency_provider.add_dependencies("foo", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("bar", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("bar", (1, 1, 0), vec![]);
dependency_provider.add_dependencies("bar", (2, 0, 0), vec![]);
dependency_provider.add_dependencies("foo", (1, 0, 0), []);
dependency_provider.add_dependencies("bar", (1, 0, 0), []);
dependency_provider.add_dependencies("bar", (1, 1, 0), []);
dependency_provider.add_dependencies("bar", (2, 0, 0), []);
// Run the algorithm.
let computed_solution = resolve(&dependency_provider, "root", (1, 0, 0)).unwrap();
@ -73,22 +89,23 @@ 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();
init_log();
let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new();
#[rustfmt::skip]
dependency_provider.add_dependencies(
"root", (1, 0, 0),
vec![("foo", Range::higher_than((1, 0, 0)))],
[("foo", Range::higher_than((1, 0, 0)))],
);
#[rustfmt::skip]
dependency_provider.add_dependencies(
"foo", (2, 0, 0),
vec![("bar", Range::between((1, 0, 0), (2, 0, 0)))],
[("bar", Range::between((1, 0, 0), (2, 0, 0)))],
);
dependency_provider.add_dependencies("foo", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("foo", (1, 0, 0), []);
#[rustfmt::skip]
dependency_provider.add_dependencies(
"bar", (1, 0, 0),
vec![("foo", Range::between((1, 0, 0), (2, 0, 0)))],
[("foo", Range::between((1, 0, 0), (2, 0, 0)))],
);
// Run the algorithm.
@ -106,12 +123,13 @@ 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();
init_log();
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(
"root", (1, 0, 0),
vec![
[
("foo", Range::between((1, 0, 0), (2, 0, 0))),
("target", Range::between((2, 0, 0), (3, 0, 0))),
],
@ -120,33 +138,33 @@ fn conflict_with_partial_satisfier() {
// foo 1.1.0 depends on left ^1.0.0 and right ^1.0.0
dependency_provider.add_dependencies(
"foo", (1, 1, 0),
vec![
[
("left", Range::between((1, 0, 0), (2, 0, 0))),
("right", Range::between((1, 0, 0), (2, 0, 0))),
],
);
dependency_provider.add_dependencies("foo", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("foo", (1, 0, 0), []);
#[rustfmt::skip]
// left 1.0.0 depends on shared >=1.0.0
dependency_provider.add_dependencies(
"left", (1, 0, 0),
vec![("shared", Range::higher_than((1, 0, 0)))],
[("shared", Range::higher_than((1, 0, 0)))],
);
#[rustfmt::skip]
// right 1.0.0 depends on shared <2.0.0
dependency_provider.add_dependencies(
"right", (1, 0, 0),
vec![("shared", Range::strictly_lower_than((2, 0, 0)))],
[("shared", Range::strictly_lower_than((2, 0, 0)))],
);
dependency_provider.add_dependencies("shared", (2, 0, 0), vec![]);
dependency_provider.add_dependencies("shared", (2, 0, 0), []);
#[rustfmt::skip]
// shared 1.0.0 depends on target ^1.0.0
dependency_provider.add_dependencies(
"shared", (1, 0, 0),
vec![("target", Range::between((1, 0, 0), (2, 0, 0)))],
[("target", Range::between((1, 0, 0), (2, 0, 0)))],
);
dependency_provider.add_dependencies("target", (2, 0, 0), vec![]);
dependency_provider.add_dependencies("target", (1, 0, 0), vec![]);
dependency_provider.add_dependencies("target", (2, 0, 0), []);
dependency_provider.add_dependencies("target", (1, 0, 0), []);
// Run the algorithm.
let computed_solution = resolve(&dependency_provider, "root", (1, 0, 0)).unwrap();
@ -171,13 +189,14 @@ fn conflict_with_partial_satisfier() {
///
/// Solution: a0, b0, c0, d0
fn double_choices() {
let mut dependency_provider = OfflineDependencyProvider::<&str, NumberVersion>::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))]);
dependency_provider.add_dependencies("c", 0, vec![]);
dependency_provider.add_dependencies("c", 1, vec![("d", Range::exact(2))]);
dependency_provider.add_dependencies("d", 0, vec![]);
init_log();
let mut dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new();
dependency_provider.add_dependencies("a", 0, [("b", Range::full()), ("c", Range::full())]);
dependency_provider.add_dependencies("b", 0, [("d", Range::singleton(0))]);
dependency_provider.add_dependencies("b", 1, [("d", Range::singleton(1))]);
dependency_provider.add_dependencies("c", 0, []);
dependency_provider.add_dependencies("c", 1, [("d", Range::singleton(2))]);
dependency_provider.add_dependencies("d", 0, []);
// Solution.
let mut expected_solution = Map::default();

View file

@ -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, SemanticVersion};
use pubgrub::version_set::VersionSet;
use proptest::collection::{btree_map, vec};
use proptest::prelude::*;
@ -24,20 +25,28 @@ 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<P: Package, V: Version>(OfflineDependencyProvider<P, V>);
struct OldestVersionsDependencyProvider<P: Package, VS: VersionSet>(
OfflineDependencyProvider<P, VS>,
);
impl<P: Package, V: Version> DependencyProvider<P, V> for OldestVersionsDependencyProvider<P, V> {
fn choose_package_version<T: std::borrow::Borrow<P>, U: std::borrow::Borrow<Range<V>>>(
impl<P: Package, VS: VersionSet> DependencyProvider<P, VS>
for OldestVersionsDependencyProvider<P, VS>
{
fn choose_package_version<T: std::borrow::Borrow<P>, U: std::borrow::Borrow<VS>>(
&self,
potential_packages: impl Iterator<Item = (T, U)>,
) -> Result<(T, Option<V>), Box<dyn Error>> {
) -> Result<(T, Option<VS::V>), Box<dyn Error + Send + Sync>> {
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<Dependencies<P, V>, Box<dyn Error>> {
fn get_dependencies(
&self,
p: &P,
v: &VS::V,
) -> Result<Dependencies<P, VS>, Box<dyn Error + Send + Sync>> {
self.0.get_dependencies(p, v)
}
}
@ -62,21 +71,25 @@ impl<DP> TimeoutDependencyProvider<DP> {
}
}
impl<P: Package, V: Version, DP: DependencyProvider<P, V>> DependencyProvider<P, V>
impl<P: Package, VS: VersionSet, DP: DependencyProvider<P, VS>> DependencyProvider<P, VS>
for TimeoutDependencyProvider<DP>
{
fn choose_package_version<T: std::borrow::Borrow<P>, U: std::borrow::Borrow<Range<V>>>(
fn choose_package_version<T: std::borrow::Borrow<P>, U: std::borrow::Borrow<VS>>(
&self,
potential_packages: impl Iterator<Item = (T, U)>,
) -> Result<(T, Option<V>), Box<dyn Error>> {
) -> Result<(T, Option<VS::V>), Box<dyn Error + Send + Sync>> {
self.dp.choose_package_version(potential_packages)
}
fn get_dependencies(&self, p: &P, v: &V) -> Result<Dependencies<P, V>, Box<dyn Error>> {
fn get_dependencies(
&self,
p: &P,
v: &VS::V,
) -> Result<Dependencies<P, VS>, Box<dyn Error + Send + Sync>> {
self.dp.get_dependencies(p, v)
}
fn should_cancel(&self) -> Result<(), Box<dyn Error>> {
fn should_cancel(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
assert!(self.start_time.elapsed().as_secs() < 60);
let calls = self.call_count.get();
assert!(calls < self.max_calls);
@ -85,11 +98,14 @@ impl<P: Package, V: Version, DP: DependencyProvider<P, V>> DependencyProvider<P,
}
}
type NumVS = Range<NumberVersion>;
type SemVS = Range<SemanticVersion>;
#[test]
#[should_panic]
fn should_cancel_can_panic() {
let mut dependency_provider = OfflineDependencyProvider::<_, NumberVersion>::new();
dependency_provider.add_dependencies(0, 0, vec![(666, Range::any())]);
let mut dependency_provider = OfflineDependencyProvider::<_, NumVS>::new();
dependency_provider.add_dependencies(0, 0, [(666, Range::full())]);
// Run the algorithm.
let _ = resolve(
@ -116,12 +132,7 @@ fn string_names() -> impl Strategy<Value = String> {
pub fn registry_strategy<N: Package + Ord>(
name: impl Strategy<Value = N>,
bad_name: N,
) -> impl Strategy<
Value = (
OfflineDependencyProvider<N, NumberVersion>,
Vec<(N, NumberVersion)>,
),
> {
) -> impl Strategy<Value = (OfflineDependencyProvider<N, NumVS>, Vec<(N, NumberVersion)>)> {
let max_crates = 40;
let max_versions = 15;
let shrinkage = 40;
@ -166,20 +177,18 @@ pub fn registry_strategy<N: Package + Ord>(
)
.prop_map(
move |(crate_vers_by_name, raw_dependencies, reverse_alphabetical, complicated_len)| {
let mut list_of_pkgid: Vec<(
(N, NumberVersion),
Option<Vec<(N, Range<NumberVersion>)>>,
)> = 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<Vec<(N, NumVS)>>)> =
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);
@ -196,13 +205,13 @@ pub fn registry_strategy<N: Package + Ord>(
deps.push((
dep_name,
if c == 0 && d == s_last_index {
Range::any()
Range::full()
} else if c == 0 {
Range::strictly_lower_than(s[d].0 + 1)
} else if d == s_last_index {
Range::higher_than(s[c].0)
} else if c == d {
Range::exact(s[c].0)
Range::singleton(s[c].0)
} else {
Range::between(s[c].0, s[d].0 + 1)
},
@ -210,7 +219,7 @@ pub fn registry_strategy<N: Package + Ord>(
}
}
let mut dependency_provider = OfflineDependencyProvider::<N, NumberVersion>::new();
let mut dependency_provider = OfflineDependencyProvider::<N, NumVS>::new();
let complicated_len = std::cmp::min(complicated_len, list_of_pkgid.len());
let complicated: Vec<_> = if reverse_alphabetical {
@ -226,7 +235,7 @@ pub fn registry_strategy<N: Package + Ord>(
dependency_provider.add_dependencies(
name,
ver,
deps.unwrap_or_else(|| vec![(bad_name.clone(), Range::any())]),
deps.unwrap_or_else(|| vec![(bad_name.clone(), Range::full())]),
);
}
@ -333,8 +342,8 @@ proptest! {
(Ok(l), Ok(r)) => assert_eq!(l, r),
(Err(PubGrubError::NoSolution(derivation_l)), Err(PubGrubError::NoSolution(derivation_r))) => {
prop_assert_eq!(
DefaultStringReporter::report(&derivation_l),
DefaultStringReporter::report(&derivation_r)
DefaultStringReporter::report(derivation_l),
DefaultStringReporter::report(derivation_r)
)},
_ => panic!("not the same result")
}
@ -373,7 +382,7 @@ proptest! {
.versions(package)
.unwrap().collect();
let version = version_idx.get(&versions);
let dependencies: Vec<(u16, Range<NumberVersion>)> = match dependency_provider
let dependencies: Vec<(u16, NumVS)> = match dependency_provider
.get_dependencies(package, version)
.unwrap()
{
@ -423,7 +432,7 @@ proptest! {
dependency_provider
.versions(&p)
.unwrap()
.map(move |v| (p, v.clone()))
.map(move |&v| (p, v))
})
.collect();
let to_remove: Set<(_, _)> = indexes_to_remove.iter().map(|x| x.get(&all_versions)).cloned().collect();
@ -432,7 +441,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 +464,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 +497,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<u16, NumberVersion> =
let dependency_provider: OfflineDependencyProvider<u16, NumVS> =
ron::de::from_str(&data).unwrap();
let mut sat = SatResolve::new(&dependency_provider);
for p in dependency_provider.packages() {
@ -501,14 +510,12 @@ 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() {
if let Ok(s) = resolve(&dependency_provider, p.clone(), n.clone()) {
if let Ok(s) = resolve(&dependency_provider, p, n.clone()) {
assert!(sat.sat_is_valid_solution(&s));
} else {
assert!(!sat.sat_resolve(p, &n));

View file

@ -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<T>() -> 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<P: Package, V: Version> {
pub struct SatResolve<P: Package, VS: VersionSet> {
solver: varisat::Solver<'static>,
all_versions_by_p: Map<P, Vec<(V, varisat::Var)>>,
all_versions_by_p: Map<P, Vec<(VS::V, varisat::Var)>>,
}
impl<P: Package, V: Version> SatResolve<P, V> {
pub fn new(dp: &OfflineDependencyProvider<P, V>) -> Self {
impl<P: Package, VS: VersionSet> SatResolve<P, VS> {
pub fn new(dp: &OfflineDependencyProvider<P, VS>) -> Self {
let mut cnf = varisat::CnfFormula::new();
let mut all_versions = vec![];
let mut all_versions_by_p: Map<P, Vec<(V, varisat::Var)>> = Map::default();
let mut all_versions_by_p: Map<P, Vec<(VS::V, varisat::Var)>> = Map::default();
for p in dp.packages() {
let mut versions_for_p = vec![];
@ -82,7 +82,7 @@ impl<P: Package, V: Version> SatResolve<P, V> {
for (p1, range) in &deps {
let empty_vec = vec![];
let mut matches: Vec<varisat::Lit> = all_versions_by_p
.get(&p1)
.get(p1)
.unwrap_or(&empty_vec)
.iter()
.filter(|(v1, _)| range.contains(v1))
@ -110,7 +110,7 @@ impl<P: Package, V: Version> SatResolve<P, V> {
}
}
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<P: Package, V: Version> SatResolve<P, V> {
}
}
pub fn sat_is_valid_solution(&mut self, pids: &SelectedDependencies<P, V>) -> bool {
pub fn sat_is_valid_solution(&mut self, pids: &SelectedDependencies<P, VS::V>) -> bool {
let mut assumption = vec![];
for (p, vs) in &self.all_versions_by_p {

View file

@ -5,16 +5,18 @@ use pubgrub::range::Range;
use pubgrub::solver::{resolve, OfflineDependencyProvider};
use pubgrub::version::NumberVersion;
type NumVS = Range<NumberVersion>;
#[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![]);
dependency_provider.add_dependencies("b", 0, vec![]);
dependency_provider.add_dependencies("b", 1, vec![("c", Range::between(0, 1))]);
dependency_provider.add_dependencies("c", 0, []);
dependency_provider.add_dependencies("c", 2, []);
dependency_provider.add_dependencies("b", 0, []);
dependency_provider.add_dependencies("b", 1, [("c", Range::between(0, 1))]);
dependency_provider.add_dependencies("a", 0, vec![("b", Range::any()), ("c", Range::any())]);
dependency_provider.add_dependencies("a", 0, [("b", Range::full()), ("c", Range::full())]);
let name = "a";
let ver = NumberVersion(0);
@ -29,14 +31,14 @@ fn same_result_on_repeated_runs() {
#[test]
fn should_always_find_a_satisfier() {
let mut dependency_provider = OfflineDependencyProvider::<_, NumberVersion>::new();
dependency_provider.add_dependencies("a", 0, vec![("b", Range::none())]);
let mut dependency_provider = OfflineDependencyProvider::<_, NumVS>::new();
dependency_provider.add_dependencies("a", 0, [("b", Range::empty())]);
assert!(matches!(
resolve(&dependency_provider, "a", 0),
Err(PubGrubError::DependencyOnTheEmptySet { .. })
));
dependency_provider.add_dependencies("c", 0, vec![("a", Range::any())]);
dependency_provider.add_dependencies("c", 0, [("a", Range::full())]);
assert!(matches!(
resolve(&dependency_provider, "c", 0),
Err(PubGrubError::DependencyOnTheEmptySet { .. })
@ -45,8 +47,8 @@ fn should_always_find_a_satisfier() {
#[test]
fn cannot_depend_on_self() {
let mut dependency_provider = OfflineDependencyProvider::<_, NumberVersion>::new();
dependency_provider.add_dependencies("a", 0, vec![("a", Range::any())]);
let mut dependency_provider = OfflineDependencyProvider::<_, NumVS>::new();
dependency_provider.add_dependencies("a", 0, [("a", Range::full())]);
assert!(matches!(
resolve(&dependency_provider, "a", 0),
Err(PubGrubError::SelfDependency { .. })