diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ed56c1f..b7c86f2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,8 +16,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - - run: cargo build --verbose - - run: cargo test --features=serde --verbose + - run: cargo build --verbose --workspace + - run: cargo test --all-features --workspace --verbose clippy: name: No warnings from Clippy @@ -30,7 +30,7 @@ jobs: - name: Check Clippy lints env: RUSTFLAGS: -D warnings - run: cargo clippy + run: cargo clippy --workspace check_formatting: name: Source code is formatted @@ -51,4 +51,4 @@ jobs: - name: Check documentation env: RUSTDOCFLAGS: -D warnings - run: cargo doc --no-deps --document-private-items + run: cargo doc --workspace --no-deps --document-private-items diff --git a/Cargo.lock b/Cargo.lock index 74daea04..5ee87d40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -579,6 +579,7 @@ dependencies = [ "serde", "thiserror", "varisat", + "version-ranges", ] [[package]] @@ -785,6 +786,9 @@ name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "syn" @@ -973,6 +977,16 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ed610a8d5e63d9c0e31300e8fdb55104c5f21e422743a9dc74848fa8317fd2" +[[package]] +name = "version-ranges" +version = "0.1.0" +dependencies = [ + "proptest", + "ron", + "serde", + "smallvec", +] + [[package]] name = "wait-timeout" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index bc96d0a5..caef5328 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,8 @@ # SPDX-License-Identifier: MPL-2.0 +[workspace] +members = ["version-ranges"] + [package] name = "pubgrub" version = "0.2.1" @@ -21,18 +24,23 @@ include = ["Cargo.toml", "LICENSE", "README.md", "src/**", "tests/**", "examples [dependencies] indexmap = "2.5.0" +log = "0.4.22" # for debug logs in tests priority-queue = "2.1.1" -thiserror = "1.0" rustc-hash = ">=1.0.0, <3.0.0" serde = { version = "1.0", features = ["derive"], optional = true } -log = "0.4.22" # for debug logs in tests +thiserror = "1.0" +version-ranges = { version = "0.1.0", path = "version-ranges" } [dev-dependencies] +criterion = "0.5" +env_logger = "0.11.5" proptest = "1.5.0" ron = "=0.9.0-alpha.0" varisat = "0.2.2" -criterion = "0.5" -env_logger = "0.11.5" +version-ranges = { version = "0.1.0", path = "version-ranges", features = ["proptest"] } + +[features] +serde = ["dep:serde", "version-ranges/serde"] [[bench]] name = "large_case" diff --git a/examples/branching_error_reporting.rs b/examples/branching_error_reporting.rs index 1a326dc1..b4ef0d8a 100644 --- a/examples/branching_error_reporting.rs +++ b/examples/branching_error_reporting.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MPL-2.0 use pubgrub::{ - resolve, DefaultStringReporter, OfflineDependencyProvider, PubGrubError, Range, Reporter, + resolve, DefaultStringReporter, OfflineDependencyProvider, PubGrubError, Ranges, Reporter, SemanticVersion, }; -type SemVS = Range; +type SemVS = Ranges; // https://github.com/dart-lang/pub/blob/master/doc/solver.md#branching-error-reporting fn main() { @@ -14,15 +14,15 @@ fn main() { // root 1.0.0 depends on foo ^1.0.0 dependency_provider.add_dependencies( "root", (1, 0, 0), - [("foo", Range::from_range_bounds((1, 0, 0)..(2, 0, 0)))], + [("foo", Ranges::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), [ - ("a", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))), - ("b", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))), + ("a", Ranges::from_range_bounds((1, 0, 0)..(2, 0, 0))), + ("b", Ranges::from_range_bounds((1, 0, 0)..(2, 0, 0))), ], ); #[rustfmt::skip] @@ -30,15 +30,15 @@ fn main() { dependency_provider.add_dependencies( "foo", (1, 1, 0), [ - ("x", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))), - ("y", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))), + ("x", Ranges::from_range_bounds((1, 0, 0)..(2, 0, 0))), + ("y", Ranges::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), - [("b", Range::from_range_bounds((2, 0, 0)..(3, 0, 0)))], + [("b", Ranges::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), []); @@ -47,7 +47,7 @@ fn main() { // x 1.0.0 depends on y ^2.0.0. dependency_provider.add_dependencies( "x", (1, 0, 0), - [("y", Range::from_range_bounds((2, 0, 0)..(3, 0, 0)))], + [("y", Ranges::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), []); diff --git a/examples/caching_dependency_provider.rs b/examples/caching_dependency_provider.rs index 7cada1a8..f6798a29 100644 --- a/examples/caching_dependency_provider.rs +++ b/examples/caching_dependency_provider.rs @@ -2,9 +2,9 @@ use std::cell::RefCell; -use pubgrub::{resolve, Dependencies, DependencyProvider, OfflineDependencyProvider, Range}; +use pubgrub::{resolve, Dependencies, DependencyProvider, OfflineDependencyProvider, Ranges}; -type NumVS = Range; +type NumVS = Ranges; // An example implementing caching dependency provider that will // store queried dependencies in memory and check them before querying more from remote. @@ -51,14 +51,14 @@ impl> DependencyProvider for CachingDependenc } } - fn choose_version(&self, package: &DP::P, range: &DP::VS) -> Result, DP::Err> { - self.remote_dependencies.choose_version(package, range) + fn choose_version(&self, package: &DP::P, ranges: &DP::VS) -> Result, DP::Err> { + self.remote_dependencies.choose_version(package, ranges) } type Priority = DP::Priority; - fn prioritize(&self, package: &DP::P, range: &DP::VS) -> Self::Priority { - self.remote_dependencies.prioritize(package, range) + fn prioritize(&self, package: &DP::P, ranges: &DP::VS) -> Self::Priority { + self.remote_dependencies.prioritize(package, ranges) } type Err = DP::Err; diff --git a/examples/doc_interface.rs b/examples/doc_interface.rs index 32d15e72..fdbd5a2f 100644 --- a/examples/doc_interface.rs +++ b/examples/doc_interface.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 -use pubgrub::{resolve, OfflineDependencyProvider, Range}; +use pubgrub::{resolve, OfflineDependencyProvider, Ranges}; -type NumVS = Range; +type NumVS = Ranges; // `root` depends on `menu` and `icons` // `menu` depends on `dropdown` @@ -12,10 +12,10 @@ type NumVS = Range; fn main() { let mut dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new(); dependency_provider.add_dependencies( - "root", 1u32, [("menu", Range::full()), ("icons", Range::full())], + "root", 1u32, [("menu", Ranges::full()), ("icons", Ranges::full())], ); - dependency_provider.add_dependencies("menu", 1u32, [("dropdown", Range::full())]); - dependency_provider.add_dependencies("dropdown", 1u32, [("icons", Range::full())]); + dependency_provider.add_dependencies("menu", 1u32, [("dropdown", Ranges::full())]); + dependency_provider.add_dependencies("dropdown", 1u32, [("icons", Ranges::full())]); dependency_provider.add_dependencies("icons", 1u32, []); // Run the algorithm. diff --git a/examples/doc_interface_error.rs b/examples/doc_interface_error.rs index 05313ad5..608f8b51 100644 --- a/examples/doc_interface_error.rs +++ b/examples/doc_interface_error.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MPL-2.0 use pubgrub::{ - resolve, DefaultStringReporter, OfflineDependencyProvider, PubGrubError, Range, Reporter, + resolve, DefaultStringReporter, OfflineDependencyProvider, PubGrubError, Ranges, Reporter, SemanticVersion, }; -type SemVS = Range; +type SemVS = Ranges; // `root` depends on `menu`, `icons 1.0.0` and `intl 5.0.0` // `menu 1.0.0` depends on `dropdown < 2.0.0` @@ -19,46 +19,46 @@ fn main() { let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new(); // Direct dependencies: menu and icons. dependency_provider.add_dependencies("root", (1, 0, 0), [ - ("menu", Range::full()), - ("icons", Range::singleton((1, 0, 0))), - ("intl", Range::singleton((5, 0, 0))), + ("menu", Ranges::full()), + ("icons", Ranges::singleton((1, 0, 0))), + ("intl", Ranges::singleton((5, 0, 0))), ]); // Dependencies of the menu lib. dependency_provider.add_dependencies("menu", (1, 0, 0), [ - ("dropdown", Range::from_range_bounds(..(2, 0, 0))), + ("dropdown", Ranges::from_range_bounds(..(2, 0, 0))), ]); dependency_provider.add_dependencies("menu", (1, 1, 0), [ - ("dropdown", Range::from_range_bounds((2, 0, 0)..)), + ("dropdown", Ranges::from_range_bounds((2, 0, 0)..)), ]); dependency_provider.add_dependencies("menu", (1, 2, 0), [ - ("dropdown", Range::from_range_bounds((2, 0, 0)..)), + ("dropdown", Ranges::from_range_bounds((2, 0, 0)..)), ]); dependency_provider.add_dependencies("menu", (1, 3, 0), [ - ("dropdown", Range::from_range_bounds((2, 0, 0)..)), + ("dropdown", Ranges::from_range_bounds((2, 0, 0)..)), ]); dependency_provider.add_dependencies("menu", (1, 4, 0), [ - ("dropdown", Range::from_range_bounds((2, 0, 0)..)), + ("dropdown", Ranges::from_range_bounds((2, 0, 0)..)), ]); dependency_provider.add_dependencies("menu", (1, 5, 0), [ - ("dropdown", Range::from_range_bounds((2, 0, 0)..)), + ("dropdown", Ranges::from_range_bounds((2, 0, 0)..)), ]); // Dependencies of the dropdown lib. dependency_provider.add_dependencies("dropdown", (1, 8, 0), [ - ("intl", Range::singleton((3, 0, 0))), + ("intl", Ranges::singleton((3, 0, 0))), ]); dependency_provider.add_dependencies("dropdown", (2, 0, 0), [ - ("icons", Range::singleton((2, 0, 0))), + ("icons", Ranges::singleton((2, 0, 0))), ]); dependency_provider.add_dependencies("dropdown", (2, 1, 0), [ - ("icons", Range::singleton((2, 0, 0))), + ("icons", Ranges::singleton((2, 0, 0))), ]); dependency_provider.add_dependencies("dropdown", (2, 2, 0), [ - ("icons", Range::singleton((2, 0, 0))), + ("icons", Ranges::singleton((2, 0, 0))), ]); dependency_provider.add_dependencies("dropdown", (2, 3, 0), [ - ("icons", Range::singleton((2, 0, 0))), + ("icons", Ranges::singleton((2, 0, 0))), ]); // Icons have no dependencies. diff --git a/examples/doc_interface_semantic.rs b/examples/doc_interface_semantic.rs index 4dcfa266..d043b9fd 100644 --- a/examples/doc_interface_semantic.rs +++ b/examples/doc_interface_semantic.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MPL-2.0 use pubgrub::{ - resolve, DefaultStringReporter, OfflineDependencyProvider, PubGrubError, Range, Reporter, + resolve, DefaultStringReporter, OfflineDependencyProvider, PubGrubError, Ranges, Reporter, SemanticVersion, }; -type SemVS = Range; +type SemVS = Ranges; // `root` depends on `menu` and `icons 1.0.0` // `menu 1.0.0` depends on `dropdown < 2.0.0` @@ -18,43 +18,43 @@ fn main() { let mut dependency_provider = OfflineDependencyProvider::<&str, SemVS>::new(); // Direct dependencies: menu and icons. dependency_provider.add_dependencies("root", (1, 0, 0), [ - ("menu", Range::full()), - ("icons", Range::singleton((1, 0, 0))), + ("menu", Ranges::full()), + ("icons", Ranges::singleton((1, 0, 0))), ]); // Dependencies of the menu lib. dependency_provider.add_dependencies("menu", (1, 0, 0), [ - ("dropdown", Range::from_range_bounds(..(2, 0, 0))), + ("dropdown", Ranges::from_range_bounds(..(2, 0, 0))), ]); dependency_provider.add_dependencies("menu", (1, 1, 0), [ - ("dropdown", Range::from_range_bounds((2, 0, 0)..)), + ("dropdown", Ranges::from_range_bounds((2, 0, 0)..)), ]); dependency_provider.add_dependencies("menu", (1, 2, 0), [ - ("dropdown", Range::from_range_bounds((2, 0, 0)..)), + ("dropdown", Ranges::from_range_bounds((2, 0, 0)..)), ]); dependency_provider.add_dependencies("menu", (1, 3, 0), [ - ("dropdown", Range::from_range_bounds((2, 0, 0)..)), + ("dropdown", Ranges::from_range_bounds((2, 0, 0)..)), ]); dependency_provider.add_dependencies("menu", (1, 4, 0), [ - ("dropdown", Range::from_range_bounds((2, 0, 0)..)), + ("dropdown", Ranges::from_range_bounds((2, 0, 0)..)), ]); dependency_provider.add_dependencies("menu", (1, 5, 0), [ - ("dropdown", Range::from_range_bounds((2, 0, 0)..)), + ("dropdown", Ranges::from_range_bounds((2, 0, 0)..)), ]); // Dependencies of the dropdown lib. dependency_provider.add_dependencies("dropdown", (1, 8, 0), []); dependency_provider.add_dependencies("dropdown", (2, 0, 0), [ - ("icons", Range::singleton((2, 0, 0))), + ("icons", Ranges::singleton((2, 0, 0))), ]); dependency_provider.add_dependencies("dropdown", (2, 1, 0), [ - ("icons", Range::singleton((2, 0, 0))), + ("icons", Ranges::singleton((2, 0, 0))), ]); dependency_provider.add_dependencies("dropdown", (2, 2, 0), [ - ("icons", Range::singleton((2, 0, 0))), + ("icons", Ranges::singleton((2, 0, 0))), ]); dependency_provider.add_dependencies("dropdown", (2, 3, 0), [ - ("icons", Range::singleton((2, 0, 0))), + ("icons", Ranges::singleton((2, 0, 0))), ]); // Icons has no dependency. diff --git a/examples/linear_error_reporting.rs b/examples/linear_error_reporting.rs index ed3f4915..490a6d87 100644 --- a/examples/linear_error_reporting.rs +++ b/examples/linear_error_reporting.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MPL-2.0 use pubgrub::{ - resolve, DefaultStringReporter, OfflineDependencyProvider, PubGrubError, Range, Reporter, + resolve, DefaultStringReporter, OfflineDependencyProvider, PubGrubError, Ranges, Reporter, SemanticVersion, }; -type SemVS = Range; +type SemVS = Ranges; // https://github.com/dart-lang/pub/blob/master/doc/solver.md#linear-error-reporting fn main() { @@ -15,21 +15,21 @@ fn main() { dependency_provider.add_dependencies( "root", (1, 0, 0), [ - ("foo", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))), - ("baz", Range::from_range_bounds((1, 0, 0)..(2, 0, 0))), + ("foo", Ranges::from_range_bounds((1, 0, 0)..(2, 0, 0))), + ("baz", Ranges::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), - [("bar", Range::from_range_bounds((2, 0, 0)..(3, 0, 0)))], + [("bar", Ranges::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), - [("baz", Range::from_range_bounds((3, 0, 0)..(4, 0, 0)))], + [("baz", Ranges::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), []); diff --git a/examples/unsat_root_message_no_version.rs b/examples/unsat_root_message_no_version.rs index fbc29aa1..3096ac70 100644 --- a/examples/unsat_root_message_no_version.rs +++ b/examples/unsat_root_message_no_version.rs @@ -4,7 +4,7 @@ use std::fmt::{self, Display}; use pubgrub::{ resolve, DefaultStringReporter, Derived, External, Map, OfflineDependencyProvider, - PubGrubError, Range, ReportFormatter, Reporter, SemanticVersion, Term, + PubGrubError, Ranges, ReportFormatter, Reporter, SemanticVersion, Term, }; #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -25,10 +25,10 @@ impl Display for Package { #[derive(Debug, Default)] struct CustomReportFormatter; -impl ReportFormatter, String> for CustomReportFormatter { +impl ReportFormatter, String> for CustomReportFormatter { type Output = String; - fn format_terms(&self, terms: &Map>>) -> String { + fn format_terms(&self, terms: &Map>>) -> String { let terms_vec: Vec<_> = terms.iter().collect(); match terms_vec.as_slice() { [] => "version solving failed".into(), @@ -38,11 +38,11 @@ impl ReportFormatter, String> for CustomReportFo [(package @ Package::Root, Term::Negative(_))] => { format!("{package} is mandatory") } - [(package @ Package::Package(_), Term::Positive(range))] => { - format!("{package} {range} is forbidden") + [(package @ Package::Package(_), Term::Positive(ranges))] => { + format!("{package} {ranges} is forbidden") } - [(package @ Package::Package(_), Term::Negative(range))] => { - format!("{package} {range} is mandatory") + [(package @ Package::Package(_), Term::Negative(ranges))] => { + format!("{package} {ranges} is mandatory") } [(p1, Term::Positive(r1)), (p2, Term::Negative(r2))] => { External::<_, _, String>::FromDependencyOf(p1, r1.clone(), p2, r2.clone()) @@ -61,32 +61,32 @@ impl ReportFormatter, String> for CustomReportFo fn format_external( &self, - external: &External, String>, + external: &External, String>, ) -> String { match external { External::NotRoot(package, version) => { format!("we are solving dependencies of {package} {version}") } External::NoVersions(package, set) => { - if set == &Range::full() { + if set == &Ranges::full() { format!("there is no available version for {package}") } else { format!("there is no version of {package} in {set}") } } External::Custom(package, set, reason) => { - if set == &Range::full() { + if set == &Ranges::full() { format!("dependencies of {package} are unavailable because {reason}") } else { format!("dependencies of {package} at version {set} are unavailable because {reason}") } } External::FromDependencyOf(package, package_set, dependency, dependency_set) => { - if package_set == &Range::full() && dependency_set == &Range::full() { + if package_set == &Ranges::full() && dependency_set == &Ranges::full() { format!("{package} depends on {dependency}") - } else if package_set == &Range::full() { + } else if package_set == &Ranges::full() { format!("{package} depends on {dependency} {dependency_set}") - } else if dependency_set == &Range::full() { + } else if dependency_set == &Ranges::full() { if matches!(package, Package::Root) { // Exclude the dummy version for root packages format!("{package} depends on {dependency}") @@ -106,9 +106,9 @@ impl ReportFormatter, String> for CustomReportFo /// Simplest case, we just combine two external incompatibilities. fn explain_both_external( &self, - external1: &External, String>, - external2: &External, String>, - current_terms: &Map>>, + external1: &External, String>, + external2: &External, String>, + current_terms: &Map>>, ) -> String { // TODO: order should be chosen to make it more logical. format!( @@ -123,10 +123,10 @@ impl ReportFormatter, String> for CustomReportFo fn explain_both_ref( &self, ref_id1: usize, - derived1: &Derived, String>, + derived1: &Derived, String>, ref_id2: usize, - derived2: &Derived, String>, - current_terms: &Map>>, + derived2: &Derived, String>, + current_terms: &Map>>, ) -> String { // TODO: order should be chosen to make it more logical. format!( @@ -145,9 +145,9 @@ impl ReportFormatter, String> for CustomReportFo fn explain_ref_and_external( &self, ref_id: usize, - derived: &Derived, String>, - external: &External, String>, - current_terms: &Map>>, + derived: &Derived, String>, + external: &External, String>, + current_terms: &Map>>, ) -> String { // TODO: order should be chosen to make it more logical. format!( @@ -162,8 +162,8 @@ impl ReportFormatter, String> for CustomReportFo /// Add an external cause to the chain of explanations. fn and_explain_external( &self, - external: &External, String>, - current_terms: &Map>>, + external: &External, String>, + current_terms: &Map>>, ) -> String { format!( "And because {}, {}.", @@ -176,8 +176,8 @@ impl ReportFormatter, String> for CustomReportFo fn and_explain_ref( &self, ref_id: usize, - derived: &Derived, String>, - current_terms: &Map>>, + derived: &Derived, String>, + current_terms: &Map>>, ) -> String { format!( "And because {} ({}), {}.", @@ -190,9 +190,9 @@ impl ReportFormatter, String> for CustomReportFo /// Add an already explained incompat to the chain of explanations. fn and_explain_prior_and_external( &self, - prior_external: &External, String>, - external: &External, String>, - current_terms: &Map>>, + prior_external: &External, String>, + external: &External, String>, + current_terms: &Map>>, ) -> String { format!( "And because {} and {}, {}.", @@ -205,14 +205,14 @@ impl ReportFormatter, String> for CustomReportFo fn main() { let mut dependency_provider = - OfflineDependencyProvider::>::new(); + OfflineDependencyProvider::>::new(); // Define the root package with a dependency on a package we do not provide dependency_provider.add_dependencies( Package::Root, (0, 0, 0), vec![( Package::Package("foo".to_string()), - Range::singleton((1, 0, 0)), + Ranges::singleton((1, 0, 0)), )], ); diff --git a/src/internal/incompatibility.rs b/src/internal/incompatibility.rs index 9740d50f..33b59cfe 100644 --- a/src/internal/incompatibility.rs +++ b/src/internal/incompatibility.rs @@ -359,7 +359,7 @@ pub(crate) mod tests { use super::*; use crate::term::tests::strategy as term_strat; - use crate::Range; + use crate::Ranges; proptest! { @@ -375,12 +375,12 @@ pub(crate) mod tests { let mut store = Arena::new(); let i1 = store.alloc(Incompatibility { package_terms: SmallMap::Two([("p1", t1.clone()), ("p2", t2.negate())]), - kind: Kind::<_, _, String>::FromDependencyOf("p1", Range::full(), "p2", Range::full()) + kind: Kind::<_, _, String>::FromDependencyOf("p1", Ranges::full(), "p2", Ranges::full()) }); let i2 = store.alloc(Incompatibility { package_terms: SmallMap::Two([("p2", t2), ("p3", t3.clone())]), - kind: Kind::<_, _, String>::FromDependencyOf("p2", Range::full(), "p3", Range::full()) + kind: Kind::<_, _, String>::FromDependencyOf("p2", Ranges::full(), "p3", Ranges::full()) }); let mut i3 = Map::default(); diff --git a/src/lib.rs b/src/lib.rs index 344ca0cd..498b5907 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,19 +40,19 @@ //! //! We can model that scenario with this library as follows //! ``` -//! # use pubgrub::{OfflineDependencyProvider, resolve, Range}; +//! # use pubgrub::{OfflineDependencyProvider, resolve, Ranges}; //! -//! type NumVS = Range; +//! type NumVS = Ranges; //! //! let mut dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new(); //! //! dependency_provider.add_dependencies( //! "root", //! 1u32, -//! [("menu", Range::full()), ("icons", Range::full())], +//! [("menu", Ranges::full()), ("icons", Ranges::full())], //! ); -//! dependency_provider.add_dependencies("menu", 1u32, [("dropdown", Range::full())]); -//! dependency_provider.add_dependencies("dropdown", 1u32, [("icons", Range::full())]); +//! dependency_provider.add_dependencies("menu", 1u32, [("dropdown", Ranges::full())]); +//! dependency_provider.add_dependencies("dropdown", 1u32, [("icons", Ranges::full())]); //! dependency_provider.add_dependencies("icons", 1u32, []); //! //! // Run the algorithm. @@ -71,14 +71,14 @@ //! and [SemanticVersion] for versions. //! This may be done quite easily by implementing the three following functions. //! ``` -//! # use pubgrub::{DependencyProvider, Dependencies, SemanticVersion,Range, DependencyConstraints, Map}; +//! # use pubgrub::{DependencyProvider, Dependencies, SemanticVersion, Ranges, DependencyConstraints, Map}; //! # use std::error::Error; //! # use std::borrow::Borrow; //! # use std::convert::Infallible; //! # //! # struct MyDependencyProvider; //! # -//! type SemVS = Range; +//! type SemVS = Ranges; //! //! impl DependencyProvider for MyDependencyProvider { //! fn choose_version(&self, package: &String, range: &SemVS) -> Result, Infallible> { @@ -172,9 +172,9 @@ //! [DefaultStringReporter] that outputs the report as a [String]. //! You may use it as follows: //! ``` -//! # use pubgrub::{resolve, OfflineDependencyProvider, DefaultStringReporter, Reporter, PubGrubError, Range}; +//! # use pubgrub::{resolve, OfflineDependencyProvider, DefaultStringReporter, Reporter, PubGrubError, Ranges}; //! # -//! # type NumVS = Range; +//! # type NumVS = Ranges; //! # //! # let dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new(); //! # let root_package = "root"; @@ -212,7 +212,6 @@ mod error; mod package; -mod range; mod report; mod solver; mod term; @@ -222,7 +221,6 @@ mod version_set; pub use error::{NoSolutionError, PubGrubError}; pub use package::Package; -pub use range::Range; pub use report::{ DefaultStringReportFormatter, DefaultStringReporter, DerivationTree, Derived, External, ReportFormatter, Reporter, @@ -231,6 +229,9 @@ pub use solver::{resolve, Dependencies, DependencyProvider, OfflineDependencyPro pub use term::Term; pub use type_aliases::{DependencyConstraints, Map, SelectedDependencies, Set}; pub use version::{SemanticVersion, VersionParseError}; +pub use version_ranges::Ranges; +#[deprecated(note = "Use `Ranges` instead")] +pub use version_ranges::Ranges as Range; pub use version_set::VersionSet; mod internal; diff --git a/src/solver.rs b/src/solver.rs index 53cb524a..2abc2e37 100644 --- a/src/solver.rs +++ b/src/solver.rs @@ -35,9 +35,9 @@ //! //! ``` //! # use std::convert::Infallible; -//! # use pubgrub::{resolve, OfflineDependencyProvider, PubGrubError, Range}; +//! # use pubgrub::{resolve, OfflineDependencyProvider, PubGrubError, Ranges}; //! # -//! # type NumVS = Range; +//! # type NumVS = Ranges; //! # //! # fn try_main() -> Result<(), PubGrubError>> { //! # let dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new(); @@ -199,7 +199,7 @@ pub trait DependencyProvider { /// How this provider stores the version requirements for the packages. /// The requirements must be able to process the same kind of version as this dependency provider. /// - /// A common choice is [`Range`][crate::range::Range]. + /// A common choice is [`Ranges`][version_ranges::Ranges]. type VS: VersionSet; /// Type for custom incompatibilities. diff --git a/src/term.rs b/src/term.rs index f67a8109..f8d51b9e 100644 --- a/src/term.rs +++ b/src/term.rs @@ -220,15 +220,14 @@ impl Display for Term { #[cfg(test)] pub mod tests { - use proptest::prelude::*; - use super::*; - use crate::Range; + use proptest::prelude::*; + use version_ranges::Ranges; - pub fn strategy() -> impl Strategy>> { + pub fn strategy() -> impl Strategy>> { prop_oneof![ - crate::range::tests::strategy().prop_map(Term::Positive), - crate::range::tests::strategy().prop_map(Term::Negative), + version_ranges::proptest_strategy().prop_map(Term::Negative), + version_ranges::proptest_strategy().prop_map(Term::Positive), ] } proptest! { diff --git a/src/version_set.rs b/src/version_set.rs index f67afb6b..bf6af37c 100644 --- a/src/version_set.rs +++ b/src/version_set.rs @@ -20,6 +20,8 @@ use std::fmt::{Debug, Display}; +use crate::Ranges; + /// Trait describing sets of versions. pub trait VersionSet: Debug + Display + Clone + Eq { /// Version type associated with the sets manipulated. @@ -68,3 +70,43 @@ pub trait VersionSet: Debug + Display + Clone + Eq { self == &self.intersection(other) } } + +impl VersionSet for Ranges { + type V = T; + + fn empty() -> Self { + Ranges::empty() + } + + fn singleton(v: Self::V) -> Self { + Ranges::singleton(v) + } + + fn complement(&self) -> Self { + Ranges::complement(self) + } + + fn intersection(&self, other: &Self) -> Self { + Ranges::intersection(self, other) + } + + fn contains(&self, v: &Self::V) -> bool { + Ranges::contains(self, v) + } + + fn full() -> Self { + Ranges::full() + } + + fn union(&self, other: &Self) -> Self { + Ranges::union(self, other) + } + + fn is_disjoint(&self, other: &Self) -> bool { + Ranges::is_disjoint(self, other) + } + + fn subset_of(&self, other: &Self) -> bool { + Ranges::subset_of(self, other) + } +} diff --git a/tests/examples.rs b/tests/examples.rs index ed2677d3..fcc237c1 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -1,12 +1,12 @@ // SPDX-License-Identifier: MPL-2.0 use pubgrub::{ - resolve, DefaultStringReporter, Map, OfflineDependencyProvider, PubGrubError, Range, + resolve, DefaultStringReporter, Map, OfflineDependencyProvider, PubGrubError, Ranges, Reporter as _, SemanticVersion, Set, }; -type NumVS = Range; -type SemVS = Range; +type NumVS = Ranges; +type SemVS = Ranges; use std::io::Write; @@ -28,12 +28,12 @@ fn no_conflict() { #[rustfmt::skip] dependency_provider.add_dependencies( "root", (1, 0, 0), - [("foo", Range::between((1, 0, 0), (2, 0, 0)))], + [("foo", Ranges::between((1, 0, 0), (2, 0, 0)))], ); #[rustfmt::skip] dependency_provider.add_dependencies( "foo", (1, 0, 0), - [("bar", Range::between((1, 0, 0), (2, 0, 0)))], + [("bar", Ranges::between((1, 0, 0), (2, 0, 0)))], ); dependency_provider.add_dependencies("bar", (1, 0, 0), []); dependency_provider.add_dependencies("bar", (2, 0, 0), []); @@ -60,14 +60,14 @@ fn avoiding_conflict_during_decision_making() { dependency_provider.add_dependencies( "root", (1, 0, 0), [ - ("foo", Range::between((1, 0, 0), (2, 0, 0))), - ("bar", Range::between((1, 0, 0), (2, 0, 0))), + ("foo", Ranges::between((1, 0, 0), (2, 0, 0))), + ("bar", Ranges::between((1, 0, 0), (2, 0, 0))), ], ); #[rustfmt::skip] dependency_provider.add_dependencies( "foo", (1, 1, 0), - [("bar", Range::between((2, 0, 0), (3, 0, 0)))], + [("bar", Ranges::between((2, 0, 0), (3, 0, 0)))], ); dependency_provider.add_dependencies("foo", (1, 0, 0), []); dependency_provider.add_dependencies("bar", (1, 0, 0), []); @@ -95,18 +95,18 @@ fn conflict_resolution() { #[rustfmt::skip] dependency_provider.add_dependencies( "root", (1, 0, 0), - [("foo", Range::higher_than((1, 0, 0)))], + [("foo", Ranges::higher_than((1, 0, 0)))], ); #[rustfmt::skip] dependency_provider.add_dependencies( "foo", (2, 0, 0), - [("bar", Range::between((1, 0, 0), (2, 0, 0)))], + [("bar", Ranges::between((1, 0, 0), (2, 0, 0)))], ); dependency_provider.add_dependencies("foo", (1, 0, 0), []); #[rustfmt::skip] dependency_provider.add_dependencies( "bar", (1, 0, 0), - [("foo", Range::between((1, 0, 0), (2, 0, 0)))], + [("foo", Ranges::between((1, 0, 0), (2, 0, 0)))], ); // Run the algorithm. @@ -131,8 +131,8 @@ fn conflict_with_partial_satisfier() { dependency_provider.add_dependencies( "root", (1, 0, 0), [ - ("foo", Range::between((1, 0, 0), (2, 0, 0))), - ("target", Range::between((2, 0, 0), (3, 0, 0))), + ("foo", Ranges::between((1, 0, 0), (2, 0, 0))), + ("target", Ranges::between((2, 0, 0), (3, 0, 0))), ], ); #[rustfmt::skip] @@ -140,8 +140,8 @@ fn conflict_with_partial_satisfier() { dependency_provider.add_dependencies( "foo", (1, 1, 0), [ - ("left", Range::between((1, 0, 0), (2, 0, 0))), - ("right", Range::between((1, 0, 0), (2, 0, 0))), + ("left", Ranges::between((1, 0, 0), (2, 0, 0))), + ("right", Ranges::between((1, 0, 0), (2, 0, 0))), ], ); dependency_provider.add_dependencies("foo", (1, 0, 0), []); @@ -149,20 +149,20 @@ fn conflict_with_partial_satisfier() { // left 1.0.0 depends on shared >=1.0.0 dependency_provider.add_dependencies( "left", (1, 0, 0), - [("shared", Range::higher_than((1, 0, 0)))], + [("shared", Ranges::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), - [("shared", Range::strictly_lower_than((2, 0, 0)))], + [("shared", Ranges::strictly_lower_than((2, 0, 0)))], ); 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), - [("target", Range::between((1, 0, 0), (2, 0, 0)))], + [("target", Ranges::between((1, 0, 0), (2, 0, 0)))], ); dependency_provider.add_dependencies("target", (2, 0, 0), []); dependency_provider.add_dependencies("target", (1, 0, 0), []); @@ -192,11 +192,11 @@ fn conflict_with_partial_satisfier() { fn double_choices() { init_log(); let mut dependency_provider = OfflineDependencyProvider::<&str, NumVS>::new(); - dependency_provider.add_dependencies("a", 0u32, [("b", Range::full()), ("c", Range::full())]); - dependency_provider.add_dependencies("b", 0u32, [("d", Range::singleton(0u32))]); - dependency_provider.add_dependencies("b", 1u32, [("d", Range::singleton(1u32))]); + dependency_provider.add_dependencies("a", 0u32, [("b", Ranges::full()), ("c", Ranges::full())]); + dependency_provider.add_dependencies("b", 0u32, [("d", Ranges::singleton(0u32))]); + dependency_provider.add_dependencies("b", 1u32, [("d", Ranges::singleton(1u32))]); dependency_provider.add_dependencies("c", 0u32, []); - dependency_provider.add_dependencies("c", 1u32, [("d", Range::singleton(2u32))]); + dependency_provider.add_dependencies("c", 1u32, [("d", Ranges::singleton(2u32))]); dependency_provider.add_dependencies("d", 0u32, []); // Solution. @@ -219,12 +219,12 @@ fn confusing_with_lots_of_holes() { dependency_provider.add_dependencies( "root", 1u32, - vec![("foo", Range::full()), ("baz", Range::full())], + vec![("foo", Ranges::full()), ("baz", Ranges::full())], ); for i in 1..6 { // foo depends on bar... - dependency_provider.add_dependencies("foo", i as u32, vec![("bar", Range::full())]); + dependency_provider.add_dependencies("foo", i as u32, vec![("bar", Ranges::full())]); } // This package is part of the dependency tree, but it's not part of the conflict diff --git a/tests/proptest.rs b/tests/proptest.rs index ee2a8e48..65d4753b 100644 --- a/tests/proptest.rs +++ b/tests/proptest.rs @@ -13,7 +13,7 @@ use proptest::string::string_regex; use pubgrub::{ resolve, DefaultStringReporter, Dependencies, DependencyProvider, DerivationTree, External, - OfflineDependencyProvider, Package, PubGrubError, Range, Reporter, SelectedDependencies, + OfflineDependencyProvider, Package, PubGrubError, Ranges, Reporter, SelectedDependencies, VersionSet, }; @@ -131,13 +131,13 @@ fn timeout_resolve( ) } -type NumVS = Range; +type NumVS = Ranges; #[test] #[should_panic] fn should_cancel_can_panic() { let mut dependency_provider = OfflineDependencyProvider::<_, NumVS>::new(); - dependency_provider.add_dependencies(0, 0u32, [(666, Range::full())]); + dependency_provider.add_dependencies(0, 0u32, [(666, Ranges::full())]); // Run the algorithm. let _ = resolve( @@ -223,17 +223,17 @@ pub fn registry_strategy( list_of_pkgid[b].1.push(( dep_name, if c > s_last_index { - Range::empty() + Ranges::empty() } else if c == 0 && d >= s_last_index { - Range::full() + Ranges::full() } else if c == 0 { - Range::strictly_lower_than(s[d] + 1) + Ranges::strictly_lower_than(s[d] + 1) } else if d >= s_last_index { - Range::higher_than(s[c]) + Ranges::higher_than(s[c]) } else if c == d { - Range::singleton(s[c]) + Ranges::singleton(s[c]) } else { - Range::between(s[c], s[d] + 1) + Ranges::between(s[c], s[d] + 1) }, )); } @@ -607,7 +607,7 @@ fn large_case() { } else if name.ends_with("str_SemanticVersion.ron") { let dependency_provider: OfflineDependencyProvider< &str, - Range, + Ranges, > = ron::de::from_str(&data).unwrap(); let mut sat = SatResolve::new(&dependency_provider); for p in dependency_provider.packages() { diff --git a/tests/tests.rs b/tests/tests.rs index 3ee9214c..8c7f0ff2 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: MPL-2.0 -use pubgrub::{resolve, OfflineDependencyProvider, PubGrubError, Range}; +use pubgrub::{resolve, OfflineDependencyProvider, PubGrubError, Ranges}; -type NumVS = Range; +type NumVS = Ranges; #[test] fn same_result_on_repeated_runs() { @@ -11,9 +11,9 @@ fn same_result_on_repeated_runs() { dependency_provider.add_dependencies("c", 0u32, []); dependency_provider.add_dependencies("c", 2u32, []); dependency_provider.add_dependencies("b", 0u32, []); - dependency_provider.add_dependencies("b", 1u32, [("c", Range::between(0u32, 1u32))]); + dependency_provider.add_dependencies("b", 1u32, [("c", Ranges::between(0u32, 1u32))]); - dependency_provider.add_dependencies("a", 0u32, [("b", Range::full()), ("c", Range::full())]); + dependency_provider.add_dependencies("a", 0u32, [("b", Ranges::full()), ("c", Ranges::full())]); let name = "a"; let ver: u32 = 0; @@ -29,13 +29,13 @@ fn same_result_on_repeated_runs() { #[test] fn should_always_find_a_satisfier() { let mut dependency_provider = OfflineDependencyProvider::<_, NumVS>::new(); - dependency_provider.add_dependencies("a", 0u32, [("b", Range::empty())]); + dependency_provider.add_dependencies("a", 0u32, [("b", Ranges::empty())]); assert!(matches!( resolve(&dependency_provider, "a", 0u32), Err(PubGrubError::NoSolution { .. }) )); - dependency_provider.add_dependencies("c", 0u32, [("a", Range::full())]); + dependency_provider.add_dependencies("c", 0u32, [("a", Ranges::full())]); assert!(matches!( resolve(&dependency_provider, "c", 0u32), Err(PubGrubError::NoSolution { .. }) @@ -45,8 +45,8 @@ fn should_always_find_a_satisfier() { #[test] fn depend_on_self() { let mut dependency_provider = OfflineDependencyProvider::<_, NumVS>::new(); - dependency_provider.add_dependencies("a", 0u32, [("a", Range::full())]); + dependency_provider.add_dependencies("a", 0u32, [("a", Ranges::full())]); assert!(resolve(&dependency_provider, "a", 0u32).is_ok()); - dependency_provider.add_dependencies("a", 66u32, [("a", Range::singleton(111u32))]); + dependency_provider.add_dependencies("a", 66u32, [("a", Ranges::singleton(111u32))]); assert!(resolve(&dependency_provider, "a", 66u32).is_err()); } diff --git a/version-ranges/Cargo.lock b/version-ranges/Cargo.lock new file mode 100644 index 00000000..519e626e --- /dev/null +++ b/version-ranges/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "version-range" +version = "0.1.0" diff --git a/version-ranges/Cargo.toml b/version-ranges/Cargo.toml new file mode 100644 index 00000000..06ea0a5a --- /dev/null +++ b/version-ranges/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "version-ranges" +version = "0.1.0" +edition = "2021" +repository = "https://github.com/pubgrub-rs/pubgrub" +license = "MPL-2.0" +keywords = ["version", "pubgrub", "selector", "ranges"] + +[dependencies] +proptest = { version = "1.5.0", optional = true } +serde = { version = "1.0.210", features = ["derive"], optional = true } +smallvec = { version = "1.13.2", features = ["union"] } + +[features] +serde = ["dep:serde", "smallvec/serde"] + +[dev-dependencies] +proptest = "1.5.0" +ron = "=0.9.0-alpha.0" diff --git a/src/range.rs b/version-ranges/src/lib.rs similarity index 74% rename from src/range.rs rename to version-ranges/src/lib.rs index 4b305e10..f8847d43 100644 --- a/src/range.rs +++ b/version-ranges/src/lib.rs @@ -1,54 +1,39 @@ // SPDX-License-Identifier: MPL-2.0 -//! Ranges are constraints defining sets of versions. +//! This crate contains a performance-optimized type for generic version ranges and operations on +//! them. //! -//! Concretely, those constraints correspond to any set of versions -//! representable as the concatenation, union, and complement -//! of the ranges building blocks. +//! [`Ranges`] can represent version selectors such as `(>=1, <2) OR (==3) OR (>4)`. Internally, +//! it is an ordered list of contiguous intervals (segments) with inclusive, exclusive or open-ended +//! ends, similar to a `Vec<(Bound, Bound)>`. //! -//! Those building blocks are: -//! - [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` +//! You can construct a basic range from one of the following build blocks. All other ranges are +//! concatenation, union, and complement of these basic ranges. +//! - [empty()](Ranges::empty): No version +//! - [full()](Ranges::full): All versions +//! - [singleton(v)](Ranges::singleton): Only the version v exactly +//! - [higher_than(v)](Ranges::higher_than): All versions `v <= versions` +//! - [strictly_higher_than(v)](Ranges::strictly_higher_than): All versions `v < versions` +//! - [lower_than(v)](Ranges::lower_than): All versions `versions <= v` +//! - [strictly_lower_than(v)](Ranges::strictly_lower_than): All versions `versions < v` +//! - [between(v1, v2)](Ranges::between): All versions `v1 <= versions < v2` //! -//! Ranges can be created from any type that implements [`Ord`] + [`Clone`]. +//! [`Ranges`] is generic over any type that implements [`Ord`] + [`Clone`] and can represent all +//! kinds of slices with ordered coordinates, not just version ranges. While built as a +//! performance-critical piece of [pubgrub](https://github.com/pubgrub-rs/pubgrub), it can be +//! adopted for other domains, too. //! -//! 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. +//! Note that there are limitations to the equality implementation: Given a `Ranges`, +//! the segments `(Unbounded, Included(42u32))` and `(Included(0), Included(42u32))` as well as +//! `(Included(1), Included(5))` and `(Included(1), Included(3)) + (Included(4), Included(5))` +//! are reported as unequal, even though the match the same versions: We can't tell that there isn't +//! a version between `0` and `-inf` or `3` and `4` respectively. //! -//! 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: +//! ## Optional features //! -//! 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 to 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. +//! * `serde`: serialization and deserialization for the version range, given that the version type +//! also supports it. +//! * `proptest`: Exports are proptest strategy for [`Ranges`]. use std::borrow::Borrow; use std::cmp::Ordering; @@ -56,67 +41,71 @@ use std::fmt::{Debug, Display, Formatter}; use std::ops::Bound::{self, Excluded, Included, Unbounded}; use std::ops::RangeBounds; -use crate::internal::SmallVec; -use crate::VersionSet; +#[cfg(any(feature = "proptest", test))] +use proptest::prelude::*; +use smallvec::{smallvec, SmallVec}; -/// A Range represents multiple intervals of a continuous range of monotone increasing -/// values. +/// Ranges 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 { - segments: SmallVec>, +pub struct Ranges { + /// Profiling in showed + /// that a single stack entry is the most efficient. This is most likely due to `Interval` + /// being large. + segments: SmallVec<[Interval; 1]>, } +// TODO: Replace the tuple type with a custom enum inlining the bounds to reduce the type's size. type Interval = (Bound, Bound); -impl Range { +impl Ranges { /// Empty set of versions. pub fn empty() -> Self { Self { - segments: SmallVec::empty(), + segments: SmallVec::new(), } } /// Set of all possible versions pub fn full() -> Self { Self { - segments: SmallVec::one((Unbounded, Unbounded)), + segments: smallvec![(Unbounded, Unbounded)], } } /// Set of all versions higher or equal to some version pub fn higher_than(v: impl Into) -> Self { Self { - segments: SmallVec::one((Included(v.into()), Unbounded)), + segments: smallvec![(Included(v.into()), Unbounded)], } } /// Set of all versions higher to some version pub fn strictly_higher_than(v: impl Into) -> Self { Self { - segments: SmallVec::one((Excluded(v.into()), Unbounded)), + segments: smallvec![(Excluded(v.into()), Unbounded)], } } /// Set of all versions lower to some version pub fn strictly_lower_than(v: impl Into) -> Self { Self { - segments: SmallVec::one((Unbounded, Excluded(v.into()))), + segments: smallvec![(Unbounded, Excluded(v.into()))], } } /// Set of all versions lower or equal to some version pub fn lower_than(v: impl Into) -> Self { Self { - segments: SmallVec::one((Unbounded, Included(v.into()))), + segments: smallvec![(Unbounded, Included(v.into()))], } } /// Set of versions greater or equal to `v1` but less than `v2`. pub fn between(v1: impl Into, v2: impl Into) -> Self { Self { - segments: SmallVec::one((Included(v1.into()), Excluded(v2.into()))), + segments: smallvec![(Included(v1.into()), Excluded(v2.into()))], } } @@ -126,16 +115,16 @@ impl Range { } } -impl Range { +impl Ranges { /// Set containing exactly one version pub fn singleton(v: impl Into) -> Self { let v = v.into(); Self { - segments: SmallVec::one((Included(v.clone()), Included(v))), + segments: smallvec![(Included(v.clone()), Included(v))], } } - /// Returns the complement of this Range. + /// Returns the complement, which contains everything not included in `self`. pub fn complement(&self) -> Self { match self.segments.first() { // Complement of ∅ is ∞ @@ -163,7 +152,7 @@ impl Range { /// Helper function performing the negation of intervals in segments. fn negate_segments(start: Bound, segments: &[Interval]) -> Self { - let mut complement_segments: SmallVec> = SmallVec::empty(); + let mut complement_segments = SmallVec::new(); let mut start = start; for (v1, v2) in segments { complement_segments.push(( @@ -190,9 +179,8 @@ impl Range { } } -impl Range { - /// If the range includes a single version, return it. - /// Otherwise, returns [None]. +impl Ranges { + /// If self contains exactly a single version, return it, otherwise, return [None]. pub fn as_singleton(&self) -> Option<&V> { match self.segments.as_slice() { [(Included(v1), Included(v2))] => { @@ -221,7 +209,7 @@ impl Range { }) } - /// Returns true if this Range contains the specified value. + /// Returns true if self contains the specified value. pub fn contains(&self, version: &V) -> bool { self.segments .binary_search_by(|segment| { @@ -233,7 +221,7 @@ impl Range { .is_ok() } - /// Returns true if this Range contains the specified values. + /// Returns true if self contains the specified values. /// /// The `versions` iterator must be sorted. /// Functionally equivalent to `versions.map(|v| self.contains(v))`. @@ -288,7 +276,7 @@ impl Range { }; if valid_segment(&start, &end) { Self { - segments: SmallVec::one((start, end)), + segments: smallvec![(start, end)], } } else { Self::empty() @@ -430,7 +418,7 @@ fn cmp_bounds_end(left: Bound<&V>, right: Bound<&V>) -> Option PartialOrd for Range { +impl PartialOrd for Ranges { /// A simple ordering scheme where we zip the segments and compare all bounds in order. If all /// bounds are equal, the longer range is considered greater. (And if all zipped bounds are /// equal and we have the same number of segments, the ranges are equal). @@ -449,7 +437,7 @@ impl PartialOrd for Range { } } -impl Ord for Range { +impl Ord for Ranges { fn cmp(&self, other: &Self) -> Ordering { self.partial_cmp(other) .expect("PartialOrd must be `Some(Ordering)` for types that implement `Ord`") @@ -574,10 +562,10 @@ fn group_adjacent_locations( }) } -impl Range { - /// Computes the union of this `Range` and another. +impl Ranges { + /// Computes the union of this `Ranges` and another. pub fn union(&self, other: &Self) -> Self { - let mut output: SmallVec> = SmallVec::empty(); + let mut output = SmallVec::new(); let mut accumulator: Option<(&Bound<_>, &Bound<_>)> = None; let mut left_iter = self.segments.iter().peekable(); let mut right_iter = other.segments.iter().peekable(); @@ -635,7 +623,7 @@ impl Range { /// Computes the intersection of two sets of versions. pub fn intersection(&self, other: &Self) -> Self { - let mut output: SmallVec> = SmallVec::empty(); + let mut output = SmallVec::new(); let mut left_iter = self.segments.iter().peekable(); let mut right_iter = other.segments.iter().peekable(); // By the definition of intersection any point that is matched by the output @@ -758,7 +746,7 @@ impl Range { true } - /// Returns a simpler Range that contains the same versions. + /// Returns a simpler representation that contains the same versions. /// /// For every one of the Versions provided in versions the existing range and the simplified range will agree on whether it is contained. /// The simplified version may include or exclude versions that are not in versions as the implementation wishes. @@ -821,8 +809,8 @@ impl Range { fn keep_segments( &self, kept_segments: impl Iterator, Option)>, - ) -> Range { - let mut segments = SmallVec::Empty; + ) -> Ranges { + let mut segments = SmallVec::new(); for (s, e) in kept_segments { segments.push(( s.map_or(Unbounded, |s| self.segments[s].0.clone()), @@ -838,49 +826,9 @@ impl Range { } } -impl VersionSet for Range { - 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) - } - - fn is_disjoint(&self, other: &Self) -> bool { - Range::is_disjoint(self, other) - } - - fn subset_of(&self, other: &Self) -> bool { - Range::subset_of(self, other) - } -} - // REPORT ###################################################################### -impl Display for Range { +impl Display for Ranges { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { if self.segments.is_empty() { write!(f, "∅")?; @@ -915,9 +863,9 @@ impl Display for Range { // SERIALIZATION ############################################################### #[cfg(feature = "serde")] -impl<'de, V: serde::Deserialize<'de>> serde::Deserialize<'de> for Range { +impl<'de, V: serde::Deserialize<'de>> serde::Deserialize<'de> for Ranges { fn deserialize>(deserializer: D) -> Result { - // This enables conversion from the "old" discrete implementation of `Range` to the new + // This enables conversion from the "old" discrete implementation of `Ranges` to the new // bounded one. // // Serialization is always performed in the new format. @@ -928,9 +876,10 @@ impl<'de, V: serde::Deserialize<'de>> serde::Deserialize<'de> for Range { D(V, Option), } - let bounds: SmallVec> = serde::Deserialize::deserialize(deserializer)?; + let bounds: SmallVec<[EitherInterval; 2]> = + serde::Deserialize::deserialize(deserializer)?; - let mut segments = SmallVec::Empty; + let mut segments = SmallVec::new(); for i in bounds { match i { EitherInterval::B(l, r) => segments.push((l, r)), @@ -939,81 +888,80 @@ impl<'de, V: serde::Deserialize<'de>> serde::Deserialize<'de> for Range { } } - Ok(Range { segments }) + Ok(Ranges { segments }) } } -// TESTS ####################################################################### - -#[cfg(test)] -pub mod tests { - use proptest::prelude::*; - - use super::*; +/// Generate version sets from a random vector of deltas between randomly inclusive or exclusive +/// bounds. +#[cfg(any(feature = "proptest", test))] +pub fn proptest_strategy() -> impl Strategy> { + ( + any::(), + 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::new(); + 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; + } + }; - /// Generate version sets from a random vector of deltas between bounds. - /// Each bound is randomly inclusive or exclusive. - pub fn strategy() -> impl Strategy> { - ( - any::(), - prop::collection::vec(any::<(u32, bool)>(), 1..10), - ) - .prop_map(|(start_unbounded, deltas)| { - let mut start = if start_unbounded { - Some(Unbounded) + let current_bound = if inclusive { + Included(largest) } else { - None + Excluded(largest) }; - 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 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)); - } + // 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)); + } - Range { segments }.check_invariants() - }) - } + Ranges { segments }.check_invariants() + }) +} + +#[cfg(test)] +pub mod tests { + use proptest::prelude::*; + + use super::*; fn version_strat() -> impl Strategy { any::() @@ -1025,7 +973,7 @@ pub mod tests { #[cfg(feature = "serde")] #[test] - fn serde_round_trip(range in strategy()) { + fn serde_round_trip(range in proptest_strategy()) { let s = ron::ser::to_string(&range).unwrap(); let r = ron::de::from_str(&s).unwrap(); assert_eq!(range, r); @@ -1034,83 +982,83 @@ pub mod tests { // Testing negate ---------------------------------- #[test] - fn negate_is_different(range in strategy()) { + fn negate_is_different(range in proptest_strategy()) { assert_ne!(range.complement(), range); } #[test] - fn double_negate_is_identity(range in strategy()) { + fn double_negate_is_identity(range in proptest_strategy()) { assert_eq!(range.complement().complement(), range); } #[test] - fn negate_contains_opposite(range in strategy(), version in version_strat()) { + fn negate_contains_opposite(range in proptest_strategy(), version in version_strat()) { assert_ne!(range.contains(&version), range.complement().contains(&version)); } // Testing intersection ---------------------------- #[test] - fn intersection_is_symmetric(r1 in strategy(), r2 in strategy()) { + fn intersection_is_symmetric(r1 in proptest_strategy(), r2 in proptest_strategy()) { assert_eq!(r1.intersection(&r2), r2.intersection(&r1)); } #[test] - fn intersection_with_any_is_identity(range in strategy()) { - assert_eq!(Range::full().intersection(&range), range); + fn intersection_with_any_is_identity(range in proptest_strategy()) { + assert_eq!(Ranges::full().intersection(&range), range); } #[test] - fn intersection_with_none_is_none(range in strategy()) { - assert_eq!(Range::empty().intersection(&range), Range::empty()); + fn intersection_with_none_is_none(range in proptest_strategy()) { + assert_eq!(Ranges::empty().intersection(&range), Ranges::empty()); } #[test] - fn intersection_is_idempotent(r1 in strategy(), r2 in strategy()) { + fn intersection_is_idempotent(r1 in proptest_strategy(), r2 in proptest_strategy()) { assert_eq!(r1.intersection(&r2).intersection(&r2), r1.intersection(&r2)); } #[test] - fn intersection_is_associative(r1 in strategy(), r2 in strategy(), r3 in strategy()) { + fn intersection_is_associative(r1 in proptest_strategy(), r2 in proptest_strategy(), r3 in proptest_strategy()) { assert_eq!(r1.intersection(&r2).intersection(&r3), r1.intersection(&r2.intersection(&r3))); } #[test] - fn intesection_of_complements_is_none(range in strategy()) { - assert_eq!(range.complement().intersection(&range), Range::empty()); + fn intesection_of_complements_is_none(range in proptest_strategy()) { + assert_eq!(range.complement().intersection(&range), Ranges::empty()); } #[test] - fn intesection_contains_both(r1 in strategy(), r2 in strategy(), version in version_strat()) { + fn intesection_contains_both(r1 in proptest_strategy(), r2 in proptest_strategy(), version in version_strat()) { assert_eq!(r1.intersection(&r2).contains(&version), r1.contains(&version) && r2.contains(&version)); } // Testing union ----------------------------------- #[test] - fn union_of_complements_is_any(range in strategy()) { - assert_eq!(range.complement().union(&range), Range::full()); + fn union_of_complements_is_any(range in proptest_strategy()) { + assert_eq!(range.complement().union(&range), Ranges::full()); } #[test] - fn union_contains_either(r1 in strategy(), r2 in strategy(), version in version_strat()) { + fn union_contains_either(r1 in proptest_strategy(), r2 in proptest_strategy(), version in version_strat()) { assert_eq!(r1.union(&r2).contains(&version), r1.contains(&version) || r2.contains(&version)); } #[test] - fn is_disjoint_through_intersection(r1 in strategy(), r2 in strategy()) { - let disjoint_def = r1.intersection(&r2) == Range::empty(); + fn is_disjoint_through_intersection(r1 in proptest_strategy(), r2 in proptest_strategy()) { + let disjoint_def = r1.intersection(&r2) == Ranges::empty(); assert_eq!(r1.is_disjoint(&r2), disjoint_def); } #[test] - fn subset_of_through_intersection(r1 in strategy(), r2 in strategy()) { + fn subset_of_through_intersection(r1 in proptest_strategy(), r2 in proptest_strategy()) { let disjoint_def = r1.intersection(&r2) == r1; assert_eq!(r1.subset_of(&r2), disjoint_def); } #[test] - fn union_through_intersection(r1 in strategy(), r2 in strategy()) { + fn union_through_intersection(r1 in proptest_strategy(), r2 in proptest_strategy()) { let union_def = r1 .complement() .intersection(&r2.complement()) @@ -1123,21 +1071,21 @@ pub mod tests { #[test] fn always_contains_exact(version in version_strat()) { - assert!(Range::singleton(version).contains(&version)); + assert!(Ranges::singleton(version).contains(&version)); } #[test] - fn contains_negation(range in strategy(), version in version_strat()) { + fn contains_negation(range in proptest_strategy(), version in version_strat()) { 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::singleton(version)) != Range::empty()); + fn contains_intersection(range in proptest_strategy(), version in version_strat()) { + assert_eq!(range.contains(&version), range.intersection(&Ranges::singleton(version)) != Ranges::empty()); } #[test] - fn contains_bounding_range(range in strategy(), version in version_strat()) { + fn contains_bounding_range(range in proptest_strategy(), version in version_strat()) { if range.contains(&version) { assert!(range.bounding_range().map(|b| b.contains(&version)).unwrap_or(false)); } @@ -1145,26 +1093,26 @@ pub mod tests { #[test] fn from_range_bounds(range in any::<(Bound, Bound)>(), version in version_strat()) { - let rv: Range<_> = Range::from_range_bounds(range); + let rv: Ranges<_> = Ranges::from_range_bounds(range); assert_eq!(range.contains(&version), rv.contains(&version)); } #[test] fn from_range_bounds_round_trip(range in any::<(Bound, Bound)>()) { - let rv: Range = Range::from_range_bounds(range); - let rv2: Range = rv.bounding_range().map(Range::from_range_bounds::<_, u32>).unwrap_or_else(Range::empty); + let rv: Ranges = Ranges::from_range_bounds(range); + let rv2: Ranges = rv.bounding_range().map(Ranges::from_range_bounds::<_, u32>).unwrap_or_else(Ranges::empty); assert_eq!(rv, rv2); } #[test] - fn contains(range in strategy(), versions in proptest::collection::vec(version_strat(), ..30)) { + fn contains(range in proptest_strategy(), versions in proptest::collection::vec(version_strat(), ..30)) { for v in versions { assert_eq!(range.contains(&v), range.segments.iter().any(|s| RangeBounds::contains(s, &v))); } } #[test] - fn contains_many(range in strategy(), mut versions in proptest::collection::vec(version_strat(), ..30)) { + fn contains_many(range in proptest_strategy(), mut versions in proptest::collection::vec(version_strat(), ..30)) { versions.sort(); assert_eq!(versions.len(), range.contains_many(versions.iter()).count()); for (a, b) in versions.iter().zip(range.contains_many(versions.iter())) { @@ -1173,7 +1121,7 @@ pub mod tests { } #[test] - fn simplify(range in strategy(), mut versions in proptest::collection::vec(version_strat(), ..30)) { + fn simplify(range in proptest_strategy(), mut versions in proptest::collection::vec(version_strat(), ..30)) { versions.sort(); let simp = range.simplify(versions.iter()); @@ -1186,7 +1134,7 @@ pub mod tests { #[test] fn contains_many_can_take_owned() { - let range: Range = Range::singleton(1); + let range: Ranges = Ranges::singleton(1); let versions = vec![1, 2, 3]; // Check that iter can be a Cow assert_eq!( @@ -1204,7 +1152,7 @@ pub mod tests { #[test] fn simplify_can_take_owned() { - let range: Range = Range::singleton(1); + let range: Ranges = Ranges::singleton(1); let versions = vec![1, 2, 3]; // Check that iter can be a Cow assert_eq!( @@ -1220,20 +1168,20 @@ pub mod tests { #[test] fn version_ord() { - let versions: &[Range] = &[ - Range::strictly_lower_than(1u32), - Range::lower_than(1u32), - Range::singleton(1u32), - Range::between(1u32, 3u32), - Range::higher_than(1u32), - Range::strictly_higher_than(1u32), - Range::singleton(2u32), - Range::singleton(2u32).union(&Range::singleton(3u32)), - Range::singleton(2u32) - .union(&Range::singleton(3u32)) - .union(&Range::singleton(4u32)), - Range::singleton(2u32).union(&Range::singleton(4u32)), - Range::singleton(3u32), + let versions: &[Ranges] = &[ + Ranges::strictly_lower_than(1u32), + Ranges::lower_than(1u32), + Ranges::singleton(1u32), + Ranges::between(1u32, 3u32), + Ranges::higher_than(1u32), + Ranges::strictly_higher_than(1u32), + Ranges::singleton(2u32), + Ranges::singleton(2u32).union(&Ranges::singleton(3u32)), + Ranges::singleton(2u32) + .union(&Ranges::singleton(3u32)) + .union(&Ranges::singleton(4u32)), + Ranges::singleton(2u32).union(&Ranges::singleton(4u32)), + Ranges::singleton(3u32), ]; let mut versions_sorted = versions.to_vec();