diff --git a/Cargo.lock b/Cargo.lock index 65ce1b60..02e5e7d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -114,9 +114,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.3.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -144,7 +144,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-executor", "async-io", "async-lock", @@ -155,9 +155,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3" +checksum = "19634d6336019ef220f09fd31168ce5c184b295cbf80345437cc36094ef223ca" dependencies = [ "async-lock", "cfg-if", @@ -168,8 +168,7 @@ dependencies = [ "polling", "rustix", "slab", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -277,11 +276,11 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ - "async-channel 2.3.1", + "async-channel 2.5.0", "async-task", "futures-io", "futures-lite", @@ -313,17 +312,40 @@ dependencies = [ "toml", ] +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "chumsky" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14377e276b2c8300513dff55ba4cc4142b44e5d6de6d00eb5b2307d650bb4ec1" +dependencies = [ + "hashbrown", + "regex-automata 0.3.9", + "serde", + "stacker", + "unicode-ident", + "unicode-segmentation", +] + [[package]] name = "clap" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" dependencies = [ "clap_builder", "clap_derive", @@ -331,9 +353,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" dependencies = [ "anstream", "anstyle", @@ -343,9 +365,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.40" +version = "4.5.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" dependencies = [ "heck", "proc-macro2", @@ -569,6 +591,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -588,6 +621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", + "futures-macro", "futures-sink", "futures-task", "pin-project-lite", @@ -603,7 +637,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "wasi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -674,6 +708,17 @@ dependencies = [ "similar", ] +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -765,6 +810,17 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -854,17 +910,16 @@ dependencies = [ [[package]] name = "polling" -version = "3.8.0" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50" +checksum = "8ee9b2fa7a4517d2c91ff5bc6c297a427a96749d15f98fcdbb22c05571a4d4b7" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix", - "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -905,6 +960,15 @@ dependencies = [ "unarray", ] +[[package]] +name = "psm" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e944464ec8536cd1beb0bbfd96987eb5e3b72f2ecdafdc5c769a37f1fa2ae1f" +dependencies = [ + "cc", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -934,9 +998,9 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", @@ -991,6 +1055,17 @@ dependencies = [ "regex-syntax 0.6.29", ] +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + [[package]] name = "regex-automata" version = "0.4.9" @@ -1008,6 +1083,12 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + [[package]] name = "regex-syntax" version = "0.8.5" @@ -1021,6 +1102,7 @@ dependencies = [ "ahash", "async-std", "bitvec", + "chumsky", "elsa", "event-listener 5.4.0", "futures", @@ -1057,15 +1139,15 @@ checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1114,9 +1196,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3" dependencies = [ "itoa", "memchr", @@ -1142,6 +1224,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "similar" version = "2.7.0" @@ -1180,6 +1268,19 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "stacker" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cddb07e32ddb770749da91081d8d0ac3a16f1a569a18b20348cd371f5dead06b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "psm", + "windows-sys 0.59.0", +] + [[package]] name = "strsim" version = "0.11.1" @@ -1236,12 +1337,16 @@ dependencies = [ [[package]] name = "tokio" -version = "1.45.1" +version = "1.46.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" +checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" dependencies = [ "backtrace", + "io-uring", + "libc", + "mio", "pin-project-lite", + "slab", ] [[package]] @@ -1379,6 +1484,12 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + [[package]] name = "unicode-width" version = "0.2.1" @@ -1427,6 +1538,12 @@ dependencies = [ "libc", ] +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + [[package]] name = "wasi" version = "0.14.2+wasi-0.2.4" @@ -1687,9 +1804,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 42494f51..efbf9ed0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ tracing = "0.1.41" elsa = "1.11.2" bitvec = "1.0.1" serde = { version = "1.0", features = ["derive"], optional = true } -futures = { version = "0.3", default-features = false, features = ["alloc"] } +futures = { version = "0.3", default-features = false, features = ["alloc", "async-await"] } event-listener = "5.4" indexmap = "2" tokio = { version = "1.45", features = ["rt"], optional = true } @@ -61,3 +61,4 @@ tracing-test = { version = "0.2.5", features = ["no-env-filter"] } tokio = { version = "1.45.1", features = ["time", "rt"] } resolvo = { path = ".", features = ["tokio", "version-ranges"] } serde_json = "1.0" +chumsky = { version = "0.10.1" , features = ["pratt"]} diff --git a/cpp/build.rs b/cpp/build.rs index 452c5fdb..c545bc7f 100644 --- a/cpp/build.rs +++ b/cpp/build.rs @@ -72,6 +72,40 @@ fn main() -> anyhow::Result<()> { constexpr Slice(const T *ptr, uintptr_t len) : ptr(ptr ? const_cast(ptr) : reinterpret_cast(sizeof(T))), len(len) {}" .to_owned(), ); + config.export.body.insert( + "ConditionalRequirement".to_owned(), + r" + /** + * Constructs a new conditional requirement with the specified condition + * and requirement. + */ + constexpr ConditionalRequirement(const ConditionId *condition, Requirement &&requirement) : condition(condition), requirement(std::forward(requirement)) {}; + /** + * Constructs a new conditional requirement without a condition. + */ + constexpr ConditionalRequirement(Requirement &&requirement) : condition(nullptr), requirement(std::forward(requirement)) {}; + ".to_owned()); + config.export.body.insert( + "Requirement".to_owned(), + r" + constexpr Requirement(VersionSetId id) : tag(Tag::Single), single({id}) {}; + constexpr Requirement(VersionSetUnionId id) : tag(Tag::Union), union_({id}) {}; + + constexpr bool is_union() const { return tag == Tag::Union; } + constexpr bool is_single() const { return tag == Tag::Single; } + " + .to_owned(), + ); + + config.export.body.insert( + "Condition".to_owned(), + r" + constexpr Condition(VersionSetId id) : tag(Tag::Requirement), requirement({id}) {}; + constexpr Condition(LogicalOperator op, ConditionId lhs, ConditionId rhs) : tag(Tag::Binary), binary({op, lhs, rhs}) {}; + + constexpr bool is_binary() const { return tag == Tag::Requirement; } + constexpr bool is_requirement() const { return tag == Tag::Binary; } + ".to_owned()); cbindgen::Builder::new() .with_config(config.clone()) diff --git a/cpp/include/resolvo.h b/cpp/include/resolvo.h index 97d00f5c..e009a3c6 100644 --- a/cpp/include/resolvo.h +++ b/cpp/include/resolvo.h @@ -7,23 +7,6 @@ namespace resolvo { using cbindgen_private::Problem; using cbindgen_private::Requirement; -/** - * Specifies a requirement (dependency) of a single version set. - */ -inline Requirement requirement_single(VersionSetId id) { - return cbindgen_private::resolvo_requirement_single(id); -} - -/** - * Specifies a requirement (dependency) of the union (logical OR) of multiple version sets. - * A solvable belonging to any of the version sets contained in the union satisfies the - * requirement. This variant is typically used for requirements that can be satisfied by two - * or more version sets belonging to different packages. - */ -inline Requirement requirement_union(VersionSetUnionId id) { - return cbindgen_private::resolvo_requirement_union(id); -} - /** * Called to solve a package problem. * @@ -44,6 +27,7 @@ inline String solve(DependencyProvider &provider, const Problem &problem, private_api::bridge_version_set_name, private_api::bridge_solvable_name, private_api::bridge_version_sets_in_union, + private_api::bridge_resolve_condition, private_api::bridge_get_candidates, private_api::bridge_sort_candidates, private_api::bridge_filter_candidates, diff --git a/cpp/include/resolvo_dependency_provider.h b/cpp/include/resolvo_dependency_provider.h index 94840794..3bac3c9f 100644 --- a/cpp/include/resolvo_dependency_provider.h +++ b/cpp/include/resolvo_dependency_provider.h @@ -7,6 +7,9 @@ namespace resolvo { using cbindgen_private::Candidates; +using cbindgen_private::Condition; +using cbindgen_private::ConditionalRequirement; +using cbindgen_private::ConditionId; using cbindgen_private::Dependencies; using cbindgen_private::ExcludedSolvable; using cbindgen_private::NameId; @@ -81,6 +84,11 @@ struct DependencyProvider { */ virtual Slice version_sets_in_union(VersionSetUnionId version_set_union_id) = 0; + /** + * Returns the condition that the given condition id describes + */ + virtual Condition resolve_condition(ConditionId condition) = 0; + /** * Obtains a list of solvables that should be considered when a package * with the given name is requested. @@ -138,14 +146,19 @@ extern "C" inline NameId bridge_version_set_name(void *data, VersionSetId versio extern "C" inline NameId bridge_solvable_name(void *data, SolvableId solvable_id) { return reinterpret_cast(data)->solvable_name(solvable_id); } +extern "C" inline void bridge_resolve_condition(void *data, ConditionId solvable_id, + Condition *result) { + *result = reinterpret_cast(data)->resolve_condition(solvable_id); +} // HACK(clang): For some reason, clang needs this to know that the return type is complete static_assert(sizeof(Slice)); -extern "C" inline Slice bridge_version_sets_in_union( - void *data, VersionSetUnionId version_set_union_id) { - return reinterpret_cast(data)->version_sets_in_union( - version_set_union_id); +extern "C" inline void bridge_version_sets_in_union(void *data, + VersionSetUnionId version_set_union_id, + Slice *result) { + *result = + reinterpret_cast(data)->version_sets_in_union(version_set_union_id); } extern "C" inline void bridge_get_candidates(void *data, NameId package, Candidates *result) { diff --git a/cpp/src/lib.rs b/cpp/src/lib.rs index 194ee01d..bdd79919 100644 --- a/cpp/src/lib.rs +++ b/cpp/src/lib.rs @@ -2,15 +2,17 @@ mod slice; mod string; mod vector; -use std::{ffi::c_void, fmt::Display, ptr::NonNull}; +use std::{ffi::c_void, fmt::Display, mem::MaybeUninit, ptr::NonNull}; use resolvo::{HintDependenciesAvailable, KnownDependencies, SolverCache}; use crate::{slice::Slice, string::String, vector::Vector}; -/// A unique identifier for a single solvable or candidate of a package. These ids should not be -/// random but rather monotonic increasing. Although it is fine to have gaps, resolvo will -/// allocate some memory based on the maximum id. +/// A unique identifier for a single solvable or candidate of a package. These +/// ids should not be random but rather monotonic increasing. Although it is +/// fine to have gaps, resolvo will allocate some memory based on the maximum +/// id. +/// /// cbindgen:derive-eq /// cbindgen:derive-neq #[repr(C)] @@ -41,10 +43,11 @@ pub enum Requirement { /// cbindgen:derive-eq /// cbindgen:derive-neq Single(VersionSetId), - /// Specifies a dependency on the union (logical OR) of multiple version sets. A solvable - /// belonging to ANY of the version sets contained in the union satisfies the requirement. - /// This variant is typically used for requirements that can be satisfied by two or more - /// version sets belonging to different packages. + /// Specifies a dependency on the union (logical OR) of multiple version + /// sets. A solvable belonging to ANY of the version sets contained in + /// the union satisfies the requirement. This variant is typically used + /// for requirements that can be satisfied by two or more version sets + /// belonging to different packages. /// cbindgen:derive-eq /// cbindgen:derive-neq Union(VersionSetUnionId), @@ -156,13 +159,120 @@ impl From for resolvo::StringId { } } +/// A unique identifier for a single condition. +/// cbindgen:derive-eq +/// cbindgen:derive-neq +#[repr(C)] +#[derive(Copy, Clone)] +pub struct ConditionId { + id: u32, +} + +impl From for ConditionId { + fn from(id: resolvo::ConditionId) -> Self { + Self { id: id.as_u32() } + } +} + +impl From for resolvo::ConditionId { + fn from(id: ConditionId) -> Self { + Self::new(id.id) + } +} + +/// Specifies the dependency of a solvable on a set of version sets. +/// cbindgen:derive-eq +/// cbindgen:derive-neq +#[repr(C)] +#[derive(Copy, Clone)] +pub enum Condition { + /// Specifies a dependency on a single version set. + /// + /// cbindgen:derive-eq + /// cbindgen:derive-neq + Requirement(VersionSetId), + + /// Combines two conditions using a logical operator. + /// + /// cbindgen:derive-eq + /// cbindgen:derive-neq + Binary(LogicalOperator, ConditionId, ConditionId), +} + +/// Defines how multiple conditions are compared to each other. +#[repr(C)] +#[derive(Copy, Clone)] +pub enum LogicalOperator { + And, + Or, +} + +impl From for LogicalOperator { + fn from(value: resolvo::LogicalOperator) -> Self { + match value { + resolvo::LogicalOperator::And => LogicalOperator::And, + resolvo::LogicalOperator::Or => LogicalOperator::Or, + } + } +} + +impl From for resolvo::LogicalOperator { + fn from(value: LogicalOperator) -> Self { + match value { + LogicalOperator::And => resolvo::LogicalOperator::And, + LogicalOperator::Or => resolvo::LogicalOperator::Or, + } + } +} + +impl From for Condition { + fn from(value: resolvo::Condition) -> Self { + match value { + resolvo::Condition::Requirement(id) => Condition::Requirement(id.into()), + resolvo::Condition::Binary(op, lhs, rhs) => { + Condition::Binary(op.into(), lhs.into(), rhs.into()) + } + } + } +} + +impl From for resolvo::Condition { + fn from(value: Condition) -> Self { + match value { + Condition::Requirement(id) => resolvo::Condition::Requirement(id.into()), + Condition::Binary(op, lhs, rhs) => { + resolvo::Condition::Binary(op.into(), lhs.into(), rhs.into()) + } + } + } +} + +#[derive(Clone)] +#[repr(C)] +pub struct ConditionalRequirement { + /// Optionally a condition that indicates whether the requirement is enabled or not. + pub condition: *const ConditionId, + + /// A requirement on another package. + pub requirement: Requirement, +} + +impl From for resolvo::ConditionalRequirement { + fn from(value: ConditionalRequirement) -> Self { + Self { + condition: unsafe { value.condition.as_ref() }.copied().map(Into::into), + requirement: value.requirement.into(), + } + } +} + #[derive(Default)] #[repr(C)] pub struct Dependencies { /// A pointer to the first element of a list of requirements. Requirements /// defines which packages should be installed alongside the depending /// package and the constraints applied to the package. - pub requirements: Vector, + pub requirements: Vector, /// Defines additional constraints on packages that may or may not be part /// of the solution. Different from `requirements`, packages in this set @@ -294,7 +404,12 @@ pub struct DependencyProvider { pub version_sets_in_union: unsafe extern "C" fn( data: *mut c_void, version_set_union_id: VersionSetUnionId, - ) -> Slice<'static, VersionSetId>, + result: NonNull>, + ), + + /// Returns the condition that the given condition id describes + pub resolve_condition: + unsafe extern "C" fn(data: *mut c_void, condition: ConditionId, result: NonNull), /// Obtains a list of solvables that should be considered when a package /// with the given name is requested. @@ -387,11 +502,32 @@ impl resolvo::Interner for &DependencyProvider { &self, version_set_union: resolvo::VersionSetUnionId, ) -> impl Iterator { - unsafe { (self.version_sets_in_union)(self.data, version_set_union.into()) } - .as_slice() - .iter() - .copied() - .map(Into::into) + let mut result = MaybeUninit::uninit(); + unsafe { + (self.version_sets_in_union)( + self.data, + version_set_union.into(), + NonNull::new_unchecked(result.as_mut_ptr()), + ); + result.assume_init() + } + .as_slice() + .iter() + .copied() + .map(Into::into) + } + + fn resolve_condition(&self, condition: resolvo::ConditionId) -> resolvo::Condition { + let mut result = MaybeUninit::uninit(); + unsafe { + (self.resolve_condition)( + self.data, + condition.into(), + NonNull::new_unchecked(result.as_mut_ptr()), + ); + result.assume_init() + } + .into() } } @@ -486,7 +622,7 @@ impl resolvo::DependencyProvider for &DependencyProvider { #[repr(C)] pub struct Problem<'a> { - pub requirements: Slice<'a, Requirement>, + pub requirements: Slice<'a, ConditionalRequirement>, pub constraints: Slice<'a, VersionSetId>, pub soft_requirements: Slice<'a, SolvableId>, } @@ -507,7 +643,7 @@ pub extern "C" fn resolvo_solve( .requirements .as_slice() .iter() - .copied() + .cloned() .map(Into::into) .collect(), ) @@ -545,20 +681,6 @@ pub extern "C" fn resolvo_solve( } } -#[unsafe(no_mangle)] -#[allow(unused)] -pub extern "C" fn resolvo_requirement_single(version_set_id: VersionSetId) -> Requirement { - Requirement::Single(version_set_id) -} - -#[unsafe(no_mangle)] -#[allow(unused)] -pub extern "C" fn resolvo_requirement_union( - version_set_union_id: VersionSetUnionId, -) -> Requirement { - Requirement::Union(version_set_union_id) -} - #[cfg(test)] mod tests { use super::*; diff --git a/cpp/tests/solve.cpp b/cpp/tests/solve.cpp index 1bb02b7b..6dab5dbd 100644 --- a/cpp/tests/solve.cpp +++ b/cpp/tests/solve.cpp @@ -30,6 +30,7 @@ struct VersionSet { struct PackageDatabase : public resolvo::DependencyProvider { resolvo::Pool names; resolvo::Pool strings; + std::vector conditions; std::vector candidates; std::vector version_sets; std::vector> version_set_unions; @@ -51,7 +52,7 @@ struct PackageDatabase : public resolvo::DependencyProvider { resolvo::Requirement alloc_requirement(std::string_view package, uint32_t version_start, uint32_t version_end) { auto id = alloc_version_set(package, version_start, version_end); - return resolvo::requirement_single(id); + return {id}; } /** @@ -67,9 +68,9 @@ struct PackageDatabase : public resolvo::DependencyProvider { version_set_union[i] = alloc_version_set(package, version_start, version_end); } - auto id = resolvo::VersionSetUnionId{static_cast(version_set_unions.size())}; + resolvo::VersionSetUnionId id = {static_cast(version_set_unions.size())}; version_set_unions.push_back(std::move(version_set_union)); - return resolvo::requirement_union(id); + return {id}; } /** @@ -83,6 +84,15 @@ struct PackageDatabase : public resolvo::DependencyProvider { return id; } + /** + * Allocates a new candidate and return the id of the candidate. + */ + resolvo::ConditionId alloc_condition(resolvo::Condition condition) { + auto id = resolvo::ConditionId{static_cast(conditions.size())}; + conditions.push_back(condition); + return id; + } + resolvo::String display_name(resolvo::NameId name) override { return resolvo::String(names[name]); } @@ -142,6 +152,10 @@ struct PackageDatabase : public resolvo::DependencyProvider { return {version_set_ids.data(), version_set_ids.size()}; } + resolvo::Condition resolve_condition(resolvo::ConditionId condition_id) override { + return conditions[condition_id.id]; + } + resolvo::Candidates get_candidates(resolvo::NameId package) override { resolvo::Candidates result; @@ -219,7 +233,8 @@ SCENARIO("Solve") { const auto d_1 = db.alloc_candidate("d", 1, {}); // Construct a problem to be solved by the solver - resolvo::Vector requirements = {db.alloc_requirement("a", 1, 3)}; + resolvo::Vector requirements = { + db.alloc_requirement("a", 1, 3)}; resolvo::Vector constraints = { db.alloc_version_set("b", 1, 3), db.alloc_version_set("c", 1, 3), @@ -239,6 +254,34 @@ SCENARIO("Solve") { REQUIRE(result[2] == c_1); } +SCENARIO("Solve conditional") { + /// Construct a database with packages a, b, and c. + PackageDatabase db; + + auto b_cond_version_set = db.alloc_version_set("b", 1, 3); + auto b_cond = db.alloc_condition({b_cond_version_set}); + auto a_cond_req = resolvo::ConditionalRequirement{&b_cond, db.alloc_requirement("a", 1, 3)}; + + auto a_1 = db.alloc_candidate("a", 1, {{}, {}}); + auto b_1 = db.alloc_candidate("b", 1, {{}, {}}); + auto c_1 = db.alloc_candidate("c", 1, {{a_cond_req}, {}}); + + // Construct a problem to be solved by the solver + resolvo::Vector requirements = { + db.alloc_requirement("b", 1, 3), db.alloc_requirement("c", 1, 3)}; + + // Solve the problem + resolvo::Vector result; + resolvo::Problem problem = {requirements, {}, {}}; + resolvo::solve(db, problem, result); + + // Check the result + REQUIRE(result.size() == 3); + REQUIRE(result[0] == c_1); + REQUIRE(result[1] == b_1); + REQUIRE(result[2] == a_1); +} + SCENARIO("Solve Union") { /// Construct a database with packages a, b, and c. PackageDatabase db; @@ -263,7 +306,7 @@ SCENARIO("Solve Union") { "f", 1, {{db.alloc_requirement("b", 1, 10)}, {db.alloc_version_set("a", 10, 20)}}); // Construct a problem to be solved by the solver - resolvo::Vector requirements = { + resolvo::Vector requirements = { db.alloc_requirement_union({{"c", 1, 10}, {"d", 1, 10}}), db.alloc_requirement("e", 1, 10), db.alloc_requirement("f", 1, 10), @@ -273,8 +316,7 @@ SCENARIO("Solve Union") { // Solve the problem resolvo::Vector result; resolvo::Problem problem = {requirements, constraints, {}}; - resolvo::solve(db, problem, result); - ; + auto error = resolvo::solve(db, problem, result); // Check the result REQUIRE(result.size() == 4); diff --git a/src/conditional_requirement.rs b/src/conditional_requirement.rs new file mode 100644 index 00000000..0477a4b5 --- /dev/null +++ b/src/conditional_requirement.rs @@ -0,0 +1,63 @@ +use crate::{Requirement, VersionSetId, VersionSetUnionId, internal::id::ConditionId}; + +/// A [`ConditionalRequirement`] is a requirement that is only enforced when a +/// certain condition holds. +#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct ConditionalRequirement { + /// The requirement is enforced only when the condition evaluates to true. + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub condition: Option, + + /// A requirement on another package. + pub requirement: Requirement, +} + +/// A condition defines a boolean expression that evaluates to true or false +/// based on whether one or more other requirements are true or false. +#[derive(Clone, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +pub enum Condition { + /// Defines a combination of conditions using logical operators. + Binary(LogicalOperator, ConditionId, ConditionId), + + /// The condition is only true if the requirement is true. + Requirement(VersionSetId), +} + +/// A [`LogicalOperator`] defines how multiple conditions are compared to each +/// other. +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] +pub enum LogicalOperator { + /// The condition is true if both operands are true. + And, + + /// The condition is true if either operand is true. + Or, +} + +// Constructs a `ConditionalRequirement` from a `Requirement` without a +// condition. +impl From for ConditionalRequirement { + fn from(value: Requirement) -> Self { + Self { + condition: None, + requirement: value, + } + } +} + +impl From for ConditionalRequirement { + fn from(value: VersionSetId) -> Self { + Requirement::Single(value).into() + } +} + +impl From for ConditionalRequirement { + fn from(value: VersionSetUnionId) -> Self { + Requirement::Union(value).into() + } +} diff --git a/src/conflict.rs b/src/conflict.rs index 1030bb84..fd2cfa48 100644 --- a/src/conflict.rs +++ b/src/conflict.rs @@ -11,7 +11,6 @@ use petgraph::{ visit::{Bfs, DfsPostOrder, EdgeRef}, }; -use crate::solver::variable_map::VariableOrigin; use crate::{ DependencyProvider, Interner, Requirement, internal::{ @@ -19,7 +18,7 @@ use crate::{ id::{ClauseId, SolvableId, SolvableOrRootId, StringId, VersionSetId}, }, runtime::AsyncRuntime, - solver::{Solver, clause::Clause}, + solver::{Solver, clause::Clause, variable_map::VariableOrigin}, }; /// Represents the cause of the solver being unable to find a solution @@ -80,7 +79,7 @@ impl Conflict { ); } Clause::Learnt(..) => unreachable!(), - &Clause::Requires(package_id, version_set_id) => { + &Clause::Requires(package_id, _condition, version_set_id) => { let solvable = package_id .as_solvable_or_root(&state.variable_map) .expect("only solvables can be excluded"); @@ -162,6 +161,12 @@ impl Conflict { ConflictEdge::Conflict(ConflictCause::Constrains(version_set_id)), ); } + Clause::AnyOf(selected, _variable) => { + // Assumption: since `AnyOf` of clause can never be false, we dont add an edge + // for it. + let decision_map = solver.state.decision_tracker.map(); + debug_assert_ne!(selected.positive().eval(decision_map), Some(false)); + } } } diff --git a/src/internal/id.rs b/src/internal/id.rs index 162b4d30..e59021d3 100644 --- a/src/internal/id.rs +++ b/src/internal/id.rs @@ -1,6 +1,6 @@ use std::{ fmt::{Display, Formatter}, - num::NonZeroU32, + num::{NonZero, NonZeroU32}, }; use crate::{Interner, internal::arena::ArenaId}; @@ -56,6 +56,36 @@ impl ArenaId for VersionSetId { } } +/// The id associated with a Condition. +#[repr(transparent)] +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct ConditionId(NonZero); + +impl ConditionId { + /// Creates a new `ConditionId` from a `u32` + pub fn new(id: u32) -> Self { + Self::from_usize(id as usize) + } + + /// Returns the inner `u32` value of the `ConditionId`. + pub fn as_u32(self) -> u32 { + self.0.get() - 1 + } +} + +impl ArenaId for ConditionId { + fn from_usize(x: usize) -> Self { + let id = (x + 1).try_into().expect("condition id too big"); + Self(unsafe { NonZero::new_unchecked(id) }) + } + + fn to_usize(self) -> usize { + (self.0.get() - 1) as usize + } +} + /// The id associated with a union (logical OR) of two or more version sets. #[repr(transparent)] #[derive(Clone, Default, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] diff --git a/src/lib.rs b/src/lib.rs index ff98b2a7..acd6bab1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ #![deny(missing_docs)] +mod conditional_requirement; pub mod conflict; pub(crate) mod internal; mod requirement; @@ -23,8 +24,9 @@ use std::{ fmt::{Debug, Display}, }; +pub use conditional_requirement::{Condition, ConditionalRequirement, LogicalOperator}; pub use internal::{ - id::{NameId, SolvableId, StringId, VersionSetId, VersionSetUnionId}, + id::{ConditionId, NameId, SolvableId, StringId, VersionSetId, VersionSetUnionId}, mapping::Mapping, }; use itertools::Itertools; @@ -99,6 +101,13 @@ pub trait Interner { &self, version_set_union: VersionSetUnionId, ) -> impl Iterator; + + /// Resolves how a condition should be represented in the solver. + /// + /// Internally, the solver uses `ConditionId` to represent conditions. This + /// allows implementers to have a custom representation for conditions that + /// differ from the representation of the solver. + fn resolve_condition(&self, condition: ConditionId) -> Condition; } /// Defines implementation specific behavior for the solver and a way for the @@ -226,7 +235,7 @@ pub struct KnownDependencies { feature = "serde", serde(default, skip_serializing_if = "Vec::is_empty") )] - pub requirements: Vec, + pub requirements: Vec, /// Defines additional constraints on packages that may or may not be part /// of the solution. Different from `requirements`, packages in this set diff --git a/src/requirement.rs b/src/requirement.rs index 610ae22a..8c5c9ec0 100644 --- a/src/requirement.rs +++ b/src/requirement.rs @@ -1,20 +1,36 @@ -use crate::{Interner, VersionSetId, VersionSetUnionId}; -use itertools::Itertools; use std::fmt::Display; +use itertools::Itertools; + +use crate::internal::id::ConditionId; +use crate::{ConditionalRequirement, Interner, VersionSetId, VersionSetUnionId}; + /// Specifies the dependency of a solvable on a set of version sets. #[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Ord, PartialOrd)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))] pub enum Requirement { /// Specifies a dependency on a single version set. Single(VersionSetId), - /// Specifies a dependency on the union (logical OR) of multiple version sets. A solvable - /// belonging to _any_ of the version sets contained in the union satisfies the requirement. - /// This variant is typically used for requirements that can be satisfied by two or more - /// version sets belonging to _different_ packages. + /// Specifies a dependency on the union (logical OR) of multiple version + /// sets. A solvable belonging to _any_ of the version sets contained in + /// the union satisfies the requirement. This variant is typically used + /// for requirements that can be satisfied by two or more version sets + /// belonging to _different_ packages. Union(VersionSetUnionId), } +impl Requirement { + /// Constructs a `ConditionalRequirement` from this `Requirement` and a + /// condition. + pub fn with_condition(self, condition: ConditionId) -> ConditionalRequirement { + ConditionalRequirement { + condition: Some(condition), + requirement: self, + } + } +} + impl Default for Requirement { fn default() -> Self { Self::Single(Default::default()) diff --git a/src/snapshot.rs b/src/snapshot.rs index 8f071d54..882e5c1e 100644 --- a/src/snapshot.rs +++ b/src/snapshot.rs @@ -15,9 +15,10 @@ use ahash::HashSet; use futures::FutureExt; use crate::{ - Candidates, Dependencies, DependencyProvider, HintDependenciesAvailable, Interner, Mapping, - NameId, Requirement, SolvableId, SolverCache, StringId, VersionSetId, VersionSetUnionId, - internal::arena::ArenaId, + Candidates, Condition, Dependencies, DependencyProvider, HintDependenciesAvailable, Interner, + Mapping, NameId, Requirement, SolvableId, SolverCache, StringId, VersionSetId, + VersionSetUnionId, + internal::{arena::ArenaId, id::ConditionId}, }; /// A single solvable in a [`DependencySnapshot`]. @@ -112,6 +113,13 @@ pub struct DependencySnapshot { serde(default, skip_serializing_if = "Mapping::is_empty") )] pub strings: Mapping, + + /// All the conditions + #[cfg_attr( + feature = "serde", + serde(default, skip_serializing_if = "Mapping::is_empty") + )] + pub conditions: Mapping, } impl DependencySnapshot { @@ -153,6 +161,7 @@ impl DependencySnapshot { VersionSet(VersionSetId), Package(NameId), String(StringId), + Condition(ConditionId), } let cache = SolverCache::new(provider); @@ -163,6 +172,7 @@ impl DependencySnapshot { version_sets: Mapping::new(), packages: Mapping::new(), strings: Mapping::new(), + conditions: Mapping::new(), }; let mut queue = names @@ -228,8 +238,13 @@ impl DependencySnapshot { } } - for &requirement in deps.requirements.iter() { - match requirement { + for requirement in deps.requirements.iter() { + if let Some(condition) = requirement.condition { + if seen.insert(Element::Condition(condition)) { + queue.push_back(Element::Condition(condition)) + } + } + match requirement.requirement { Requirement::Single(version_set) => { if seen.insert(Element::VersionSet(version_set)) { queue.push_back(Element::VersionSet(version_set)); @@ -299,6 +314,24 @@ impl DependencySnapshot { result.version_sets.insert(version_set_id, version_set); } + Element::Condition(condition_id) => { + let condition = cache.provider().resolve_condition(condition_id); + match condition { + Condition::Requirement(version_set) => { + if seen.insert(Element::VersionSet(version_set)) { + queue.push_back(Element::VersionSet(version_set)) + } + } + Condition::Binary(_, lhs, rhs) => { + for cond in [lhs, rhs] { + if seen.insert(Element::Condition(cond)) { + queue.push_back(Element::Condition(cond)) + } + } + } + } + result.conditions.insert(condition_id, condition); + } } } @@ -456,6 +489,14 @@ impl Interner for SnapshotProvider<'_> { .iter() .copied() } + + fn resolve_condition(&self, condition: ConditionId) -> Condition { + self.snapshot + .conditions + .get(condition) + .expect("missing condition") + .clone() + } } impl DependencyProvider for SnapshotProvider<'_> { diff --git a/src/solver/binary_encoding.rs b/src/solver/binary_encoding.rs index 144d93c2..49a5261c 100644 --- a/src/solver/binary_encoding.rs +++ b/src/solver/binary_encoding.rs @@ -6,8 +6,8 @@ use indexmap::IndexSet; /// that at most one of a set of variables can be true. pub(crate) struct AtMostOnceTracker { /// The set of variables of which at most one can be assigned true. - variables: IndexSet, - helpers: Vec, + pub(crate) variables: IndexSet, + pub(crate) helpers: Vec, } impl Default for AtMostOnceTracker { @@ -31,10 +31,10 @@ impl AtMostOnceTracker { variable: V, mut alloc_clause: impl FnMut(V, V, bool), mut alloc_var: impl FnMut() -> V, - ) { + ) -> bool { // If the variable is already tracked, we don't need to do anything. if self.variables.contains(&variable) { - return; + return false; } // If there are no variables yet, it means that this is the first variable that @@ -42,7 +42,7 @@ impl AtMostOnceTracker { // need to add any clauses. if self.variables.is_empty() { self.variables.insert(variable.clone()); - return; + return true; } // Allocate additional helper variables as needed and create clauses for the @@ -69,6 +69,8 @@ impl AtMostOnceTracker { ((var_idx >> bit_idx) & 1) == 1, ); } + + true } } diff --git a/src/solver/clause.rs b/src/solver/clause.rs index 8a28d2ed..e798818e 100644 --- a/src/solver/clause.rs +++ b/src/solver/clause.rs @@ -7,6 +7,7 @@ use std::{ use elsa::FrozenMap; +use crate::solver::conditions::Disjunction; use crate::{ Interner, NameId, Requirement, internal::{ @@ -14,8 +15,8 @@ use crate::{ id::{ClauseId, LearntClauseId, StringId, VersionSetId}, }, solver::{ - VariableId, decision_map::DecisionMap, decision_tracker::DecisionTracker, - variable_map::VariableMap, + VariableId, conditions::DisjunctionId, decision_map::DecisionMap, + decision_tracker::DecisionTracker, variable_map::VariableMap, }, }; @@ -55,9 +56,13 @@ pub(crate) enum Clause { /// Makes the solvable require the candidates associated with the /// [`Requirement`]. /// - /// In SAT terms: (¬A ∨ B1 ∨ B2 ∨ ... ∨ B99), where B1 to B99 represent the - /// possible candidates for the provided [`Requirement`]. - Requires(VariableId, Requirement), + /// Optionally the requirement can be associated with a condition in the + /// form of a disjunction. + /// + /// In SAT terms: (¬A ∨ ¬D1 v ¬D2 .. v ¬D99 v B1 ∨ B2 ∨ ... ∨ B99), where D1 + /// to D99 represent the candidates of the disjunction and B1 to B99 + /// represent the possible candidates for the provided [`Requirement`]. + Requires(VariableId, Option, Requirement), /// Ensures only a single version of a package is installed /// /// Usage: generate one [`Clause::ForbidMultipleInstances`] clause for each @@ -97,6 +102,10 @@ pub(crate) enum Clause { /// A clause that forbids a package from being installed for an external /// reason. Excluded(VariableId, StringId), + + /// A clause that indicates that any version of a package C is selected. + /// In SAT terms: (C_selected v ¬Cj) + AnyOf(VariableId, VariableId), } impl Clause { @@ -116,37 +125,47 @@ impl Clause { parent: VariableId, requirement: Requirement, candidates: impl IntoIterator, + condition: Option<(DisjunctionId, &[Literal])>, decision_tracker: &DecisionTracker, ) -> (Self, Option<[Literal; 2]>, bool) { // It only makes sense to introduce a requires clause when the parent solvable // is undecided or going to be installed assert_ne!(decision_tracker.assigned_value(parent), Some(false)); - let kind = Clause::Requires(parent, requirement); - let mut candidates = candidates.into_iter().peekable(); - let first_candidate = candidates.peek().copied(); - if let Some(first_candidate) = first_candidate { - match candidates.find(|&c| decision_tracker.assigned_value(c) != Some(false)) { - // Watch any candidate that is not assigned to false - Some(watched_candidate) => ( - kind, - Some([parent.negative(), watched_candidate.positive()]), - false, - ), - - // All candidates are assigned to false! Therefore, the clause conflicts with the - // current decisions. There are no valid watches for it at the moment, but we will - // assign default ones nevertheless, because they will become valid after the solver - // restarts. - None => ( - kind, - Some([parent.negative(), first_candidate.positive()]), - true, - ), + let kind = Clause::Requires(parent, condition.map(|d| d.0), requirement); + + // Construct literals to watch + let condition_literals = condition + .into_iter() + .flat_map(|(_, candidates)| candidates) + .copied() + .peekable(); + let candidate_literals = candidates + .into_iter() + .map(|candidate| candidate.positive()) + .peekable(); + + let mut literals = condition_literals.chain(candidate_literals).peekable(); + let Some(&first_literal) = literals.peek() else { + // If there are no candidates and there is no condition, then this clause is an + // assertion. + // There is also no conflict because we asserted above that the parent is not + // assigned to false yet. + return (kind, None, false); + }; + + match literals.find(|&c| c.eval(decision_tracker.map()) != Some(false)) { + // Watch any candidate that is not assigned to false + Some(watched_candidate) => (kind, Some([parent.negative(), watched_candidate]), false), + + // All candidates are assigned to false! Therefore, the clause conflicts with the + // current decisions. There are no valid watches for it at the moment, but we will + // assign default ones nevertheless, because they will become valid after the solver + // restarts. + None => { + // Try to find a condition that is not assigned to false. + (kind, Some([parent.negative(), first_literal]), true) } - } else { - // If there are no candidates there is no need to watch anything. - (kind, None, false) } } @@ -230,6 +249,11 @@ impl Clause { ) } + fn any_of(selected_var: VariableId, other_var: VariableId) -> (Self, Option<[Literal; 2]>) { + let kind = Clause::AnyOf(selected_var, other_var); + (kind, Some([selected_var.positive(), other_var.negative()])) + } + /// Tries to fold over all the literals in the clause. /// /// This function is useful to iterate, find, or filter the literals in a @@ -242,6 +266,7 @@ impl Clause { Vec>, ahash::RandomState, >, + disjunction_to_candidates: &Arena, init: C, mut visit: F, ) -> ControlFlow @@ -255,14 +280,22 @@ impl Clause { .iter() .copied() .try_fold(init, visit), - Clause::Requires(solvable_id, match_spec_id) => iter::once(solvable_id.negative()) - .chain( - requirements_to_sorted_candidates[&match_spec_id] - .iter() - .flatten() - .map(|&s| s.positive()), - ) - .try_fold(init, visit), + Clause::Requires(solvable_id, disjunction, match_spec_id) => { + iter::once(solvable_id.negative()) + .chain( + disjunction + .into_iter() + .flat_map(|d| disjunction_to_candidates[d].literals.iter()) + .copied(), + ) + .chain( + requirements_to_sorted_candidates[&match_spec_id] + .iter() + .flatten() + .map(|&s| s.positive()), + ) + .try_fold(init, visit) + } Clause::Constrains(s1, s2, _) => [s1.negative(), s2.negative()] .into_iter() .try_fold(init, visit), @@ -272,6 +305,9 @@ impl Clause { Clause::Lock(_, s) => [s.negative(), VariableId::root().negative()] .into_iter() .try_fold(init, visit), + Clause::AnyOf(selected, variable) => [selected.positive(), variable.negative()] + .into_iter() + .try_fold(init, visit), } } @@ -286,11 +322,13 @@ impl Clause { Vec>, ahash::RandomState, >, + disjunction_to_candidates: &Arena, mut visit: impl FnMut(Literal), ) { self.try_fold_literals( learnt_clauses, requirements_to_sorted_candidates, + disjunction_to_candidates, (), |_, lit| { visit(lit); @@ -348,12 +386,14 @@ impl WatchedLiterals { candidate: VariableId, requirement: Requirement, matching_candidates: impl IntoIterator, + condition: Option<(DisjunctionId, &[Literal])>, decision_tracker: &DecisionTracker, ) -> (Option, bool, Clause) { let (kind, watched_literals, conflict) = Clause::requires( candidate, requirement, matching_candidates, + condition, decision_tracker, ); @@ -419,6 +459,11 @@ impl WatchedLiterals { (Self::from_kind_and_initial_watches(watched_literals), kind) } + pub fn any_of(selected_var: VariableId, other_var: VariableId) -> (Option, Clause) { + let (kind, watched_literals) = Clause::any_of(selected_var, other_var); + (Self::from_kind_and_initial_watches(watched_literals), kind) + } + fn from_kind_and_initial_watches(watched_literals: Option<[Literal; 2]>) -> Option { let watched_literals = watched_literals?; debug_assert!(watched_literals[0] != watched_literals[1]); @@ -437,6 +482,7 @@ impl WatchedLiterals { Vec>, ahash::RandomState, >, + disjunction_to_candidates: &Arena, decision_map: &DecisionMap, for_watch_index: usize, ) -> Option { @@ -453,6 +499,7 @@ impl WatchedLiterals { let next = clause.try_fold_literals( learnt_clauses, requirement_to_sorted_candidates, + disjunction_to_candidates, (), |_, lit| { // The next unwatched variable (if available), is a variable that is: @@ -570,13 +617,14 @@ impl Display for ClauseDisplay<'_, I> { ) } Clause::Learnt(learnt_id) => write!(f, "Learnt({learnt_id:?})"), - Clause::Requires(variable, requirement) => { + Clause::Requires(variable, condition, requirement) => { write!( f, - "Requires({}({:?}), {})", + "Requires({}({:?}), {}, condition={:?})", variable.display(self.variable_map, self.interner), variable, requirement.display(self.interner), + condition ) } Clause::Constrains(v1, v2, version_set_id) => { @@ -611,6 +659,16 @@ impl Display for ClauseDisplay<'_, I> { other, ) } + Clause::AnyOf(variable, other) => { + write!( + f, + "AnyOf({}({:?}), {}({:?}))", + variable.display(self.variable_map, self.interner), + variable, + other.display(self.variable_map, self.interner), + other, + ) + } } } } @@ -664,6 +722,7 @@ mod test { parent, VersionSetId::from_usize(0).into(), [candidate1, candidate2], + None, &decisions, ); assert!(!conflict); @@ -687,6 +746,7 @@ mod test { parent, VersionSetId::from_usize(0).into(), [candidate1, candidate2], + None, &decisions, ); assert!(!conflict); @@ -710,6 +770,7 @@ mod test { parent, VersionSetId::from_usize(0).into(), [candidate1, candidate2], + None, &decisions, ); assert!(conflict); @@ -731,6 +792,7 @@ mod test { parent, VersionSetId::from_usize(0).into(), [candidate1, candidate2], + None, &decisions, ) }) diff --git a/src/solver/conditions.rs b/src/solver/conditions.rs new file mode 100644 index 00000000..e131e719 --- /dev/null +++ b/src/solver/conditions.rs @@ -0,0 +1,124 @@ +//! Resolvo supports conditional dependencies. E.g. `foo REQUIRES bar IF baz is +//! selected`. +//! +//! In SAT terms we express the requirement `A requires B` as `¬A1 v B1 .. v +//! B99` where A1 is a candidate of package A and B1 through B99 are candidates +//! that match the requirement B. In logical terms we say, either we do not +//! select A1 or we select one of matching Bs. +//! +//! If we add a condition C to the requirement, e.g. `A requires B if C` we can +//! modify the requirement clause to `¬A1 v ¬(C) v B1 .. v B2`. In logical terms +//! we say, either we do not select A1 or we do not match the condition or we +//! select one of the matching Bs. +//! +//! The condition `C` however expands to another set of matching candidates +//! (e.g. `C1 v C2 v C3`). If we insert that into the formula we get, +//! +//! `¬A1 v ¬(C1 v C2 v C3) v B1 .. v B2` +//! +//! which expands to +//! +//! `¬A1 v ¬C1 ^ ¬C2 ^ ¬C3 v B1 .. v B2` +//! +//! This is not in CNF form (required for SAT clauses) so we cannot use this +//! directly. To work around that problem we instead represent the condition +//! `¬(C)` as the complement of the version set C. E.g. if C would represent +//! `package >=1` then the complement would represent the candidates that match +//! `package <1`, or the candidates that do NOT match C. So if we represent the +//! complement of C as C! the final form of clause becomes: +//! +//! `¬A1 v C!1 .. v C!99 v B1 .. v B2` +//! +//! This introduces another edge case though. What if the complement is empty? +//! The final format would be void of `C!n` variables so it would become `¬A1 v +//! B1 .. v B2`, e.g. A unconditionally requires B. To fix that issue we +//! introduce an auxiliary variable that encodes if at-least-one of the package +//! C is selected (notated as `C_selected`). For each candidate of C we add the +//! clause +//! +//! `¬Cn v C_selected` +//! +//! This forces `C_selected` to become true if any `Cn` is set to true. We then +//! modify the requirement clause to be +//! +//! `¬A1 v ¬C_selected v B1 .. v B2` +//! +//! Note that we do not encode any clauses to force `C_selected` to be false. +//! We argue that this state is not required to function properly. If +//! `C_selected` would be set to false it would mean that all candidates of +//! package C are unselectable. This would disable the requirement, e.g. B +//! shouldnt be selected for A1. But it doesnt prevent A1 from being selected. +//! +//! Conditions are expressed as boolean expression trees. Internally they are +//! converted to Disjunctive Normal Form (DNF). A boolean expression is +//! in DNF if it is a **disjunction (OR)** of one or more **conjunctive clauses +//! (AND)**. +//! +//! We add a requires clause for each disjunction in the boolean expression. So +//! if we have the requirement `A requires B if C or D` we add requires clause +//! for `A requires B if C` and for `A requires B if D`. + +use std::num::NonZero; + +use crate::solver::clause::Literal; +use crate::{ + Condition, ConditionId, Interner, LogicalOperator, VersionSetId, internal::arena::ArenaId, +}; + +/// An identifier that describes a group of version sets that are combined using +/// AND logical operators. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct DisjunctionId(NonZero); + +impl ArenaId for DisjunctionId { + fn from_usize(x: usize) -> Self { + // Safe because we are guaranteed that the id is non-zero by adding 1. + DisjunctionId(unsafe { NonZero::new_unchecked((x + 1) as u32) }) + } + + fn to_usize(self) -> usize { + (self.0.get() - 1) as usize + } +} + +pub struct Disjunction { + /// The literals associated with this particular disjunction + pub literals: Vec, + + /// The top-level condition to which this disjunction belongs. + pub _condition: ConditionId, +} + +/// Converts from a boolean expression tree as described by `condition` to a +/// boolean formula in disjunctive normal form (DNF). Each inner Vec represents +/// a conjunction (AND group) and the outer Vec represents the disjunction (OR +/// group). +pub fn convert_conditions_to_dnf( + condition: ConditionId, + interner: &I, +) -> Vec> { + match interner.resolve_condition(condition) { + Condition::Requirement(version_set) => vec![vec![version_set]], + Condition::Binary(LogicalOperator::Or, lhs, rhs) => { + let mut left_dnf = convert_conditions_to_dnf(lhs, interner); + let mut right_dnf = convert_conditions_to_dnf(rhs, interner); + left_dnf.append(&mut right_dnf); + left_dnf + } + Condition::Binary(LogicalOperator::And, lhs, rhs) => { + let left_dnf = convert_conditions_to_dnf(lhs, interner); + let right_dnf = convert_conditions_to_dnf(rhs, interner); + + // Distribute AND over OR + let mut result = Vec::new(); + for l in &left_dnf { + for r in &right_dnf { + let mut merged = l.clone(); + merged.extend(r.clone()); + result.push(merged); + } + } + result + } + } +} diff --git a/src/solver/encoding.rs b/src/solver/encoding.rs index 1340c169..9daab6bc 100644 --- a/src/solver/encoding.rs +++ b/src/solver/encoding.rs @@ -1,12 +1,20 @@ -use super::{SolverState, clause::WatchedLiterals}; +use std::{any::Any, future::ready}; + +use futures::{ + FutureExt, StreamExt, TryFutureExt, future::LocalBoxFuture, stream::FuturesUnordered, +}; +use indexmap::IndexMap; + +use super::{SolverState, clause::WatchedLiterals, conditions}; use crate::{ - Candidates, Dependencies, DependencyProvider, NameId, Requirement, SolvableId, SolverCache, - StringId, VersionSetId, - internal::arena::ArenaId, - internal::id::{ClauseId, SolvableOrRootId, VariableId}, + Candidates, ConditionId, ConditionalRequirement, Dependencies, DependencyProvider, NameId, + SolvableId, SolverCache, StringId, VersionSetId, + internal::{ + arena::ArenaId, + id::{ClauseId, SolvableOrRootId, VariableId}, + }, + solver::{conditions::Disjunction, decision::Decision}, }; -use futures::{FutureExt, StreamExt, future::LocalBoxFuture, stream::FuturesUnordered}; -use std::any::Any; /// An object that is responsible for encoding information from the dependency /// provider into rules and variables that are used by the solver. @@ -19,18 +27,27 @@ use std::any::Any; /// /// The encoder itself is completely single threaded (and not `Send`) but the /// dependency provider is free to spawn tasks on other threads. -pub(crate) struct Encoder<'a, D: DependencyProvider> { +pub(crate) struct Encoder<'a, 'cache, D: DependencyProvider> { state: &'a mut SolverState, - cache: &'a SolverCache, + cache: &'cache SolverCache, + level: u32, /// The dependencies of the root solvable. - root_dependencies: &'a Dependencies, + root_dependencies: &'cache Dependencies, /// The set of futures that are pending to be resolved. - pending_futures: FuturesUnordered, Box>>>, + pending_futures: + FuturesUnordered, Box>>>, - /// A list of clauses that were introduced that are conflicting with the current state. + /// A list of clauses that were introduced that are conflicting with the + /// current state. conflicting_clauses: Vec, + + /// Stores for which packages and solvables we want to add forbid clauses. + pending_forbid_clauses: IndexMap, ahash::RandomState>, + + /// A set of packages that should have an at-least-once tracker. + new_at_least_one_packages: IndexMap, } /// The result of a future that was queued for processing. @@ -56,8 +73,15 @@ struct CandidatesAvailable<'a> { /// Result of querying candidates for a particular requirement. struct RequirementCandidatesAvailable<'a> { solvable_id: SolvableOrRootId, - requirement: Requirement, + requirement: ConditionalRequirement, candidates: Vec<&'a [SolvableId]>, + condition: Option<(ConditionId, Vec>>)>, +} + +/// The complement of a solvables that match aVersionSet or an empty set. +enum DisjunctionComplement<'a> { + Solvables(VersionSetId, &'a [SolvableId]), + Empty(VersionSetId), } /// Result of querying candidates for a particular constraint. @@ -67,11 +91,12 @@ struct ConstraintCandidatesAvailable<'a> { candidates: &'a [SolvableId], } -impl<'a, D: DependencyProvider> Encoder<'a, D> { +impl<'a, 'cache, D: DependencyProvider> Encoder<'a, 'cache, D> { pub fn new( state: &'a mut SolverState, - cache: &'a SolverCache, - root_dependencies: &'a Dependencies, + cache: &'cache SolverCache, + root_dependencies: &'cache Dependencies, + level: u32, ) -> Self { Self { state, @@ -79,6 +104,9 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { root_dependencies, pending_futures: FuturesUnordered::new(), conflicting_clauses: Vec::new(), + pending_forbid_clauses: IndexMap::default(), + level, + new_at_least_one_packages: IndexMap::default(), } } @@ -97,11 +125,14 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { self.on_task_result(future_result?); } + self.add_pending_forbid_clauses(); + self.add_pending_at_least_one_clauses(); + Ok(self.conflicting_clauses) } /// Called when the result of a future is available. - fn on_task_result(&mut self, result: TaskResult<'a>) { + fn on_task_result(&mut self, result: TaskResult<'cache>) { match result { TaskResult::Dependencies(dependencies) => self.on_dependencies_available(dependencies), TaskResult::Candidates(candidates) => self.on_candidates_available(candidates), @@ -126,7 +157,7 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { DependenciesAvailable { solvable_id, dependencies, - }: DependenciesAvailable<'a>, + }: DependenciesAvailable<'cache>, ) { tracing::trace!( "dependencies available for {}", @@ -137,7 +168,8 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { let (requirements, constraints) = match dependencies { Dependencies::Known(deps) => (&deps.requirements, &deps.constrains), Dependencies::Unknown(reason) => { - // If the dependencies are unknown, we add an exclusion clause and stop processing. + // If the dependencies are unknown, we add an exclusion clause and stop + // processing. self.add_exclusion_clause(solvable_id, *reason); return; } @@ -147,7 +179,7 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { // refer. Make sure we have all candidates for a particular package. for version_set_id in requirements .iter() - .flat_map(|requirement| requirement.version_sets(self.cache.provider())) + .flat_map(|requirement| requirement.requirement.version_sets(self.cache.provider())) .chain(constraints.iter().copied()) { let package_name = self.cache.provider().version_set_name(version_set_id); @@ -155,8 +187,8 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { } // For each requirement request the matching candidates. - for &requirement in requirements { - self.queue_requirement(solvable_id, requirement); + for requirement in requirements { + self.queue_conditional_requirement(solvable_id, requirement.clone()); } // For each constraint, request the candidates that are non-matching @@ -172,7 +204,7 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { CandidatesAvailable { name_id, package_candidates, - }: CandidatesAvailable<'a>, + }: CandidatesAvailable<'cache>, ) { tracing::trace!( "Package candidates available for {}", @@ -206,11 +238,12 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { solvable_id, requirement, candidates, - }: RequirementCandidatesAvailable<'a>, + condition, + }: RequirementCandidatesAvailable<'cache>, ) { tracing::trace!( "Sorted candidates available for {}", - requirement.display(self.cache.provider()), + requirement.requirement.display(self.cache.provider()), ); let variable = self.state.variable_map.intern_solvable_or_root(solvable_id); @@ -226,90 +259,129 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { }) .collect::>(); - // Queue requesting the dependencies of the candidates as well if they are - // cheaply available from the dependency provider. - for (candidate, candidate_var) in candidates + // Make sure that for every candidate that we require we also have a forbid + // clause to force one solvable per package name. + // + // We only add these clauses for packages that can actually be selected to + // reduce the overall number of clauses. + for (solvable, variable_id) in candidates .iter() .zip(version_set_variables.iter()) .flat_map(|(&candidates, variable)| { candidates.iter().copied().zip(variable.iter().copied()) }) { + let name_id = self.cache.provider().solvable_name(solvable); + self.pending_forbid_clauses + .entry(name_id) + .or_default() + .push(variable_id); + } + + // Queue requesting the dependencies of the candidates as well if they are + // cheaply available from the dependency provider. + for &candidate in candidates.iter().flat_map(|solvables| solvables.iter()) { // If the dependencies are already available for the // candidate, queue the candidate for processing. if self.cache.are_dependencies_available_for(candidate) { self.queue_solvable(candidate.into()) } + } - // Add forbid constraints for this solvable on all other - // solvables that have been visited already for the same - // version set name. - let name_id = self.cache.provider().solvable_name(candidate); - let other_solvables = self - .state - .forbidden_clauses_added - .entry(name_id) - .or_default(); - other_solvables.add( - candidate_var, - |a, b, positive| { - let (watched_literals, kind) = WatchedLiterals::forbid_multiple( - a, - if positive { b.positive() } else { b.negative() }, - name_id, - ); - let clause_id = self.state.clauses.alloc(watched_literals, kind); - let watched_literals = self.state.clauses.watched_literals - [clause_id.to_usize()] - .as_mut() - .expect("forbid clause must have watched literals"); - self.state - .watches - .start_watching(watched_literals, clause_id); - }, - || { - self.state - .variable_map - .alloc_forbid_multiple_variable(name_id) - }, - ); + // Determine the disjunctions of all the conditions for this requirement. + let mut conditions = Vec::with_capacity(condition.as_ref().map_or(0, |(_, dnf)| dnf.len())); + if let Some((condition, dnf)) = condition { + for disjunctions in dnf { + let mut disjunction_literals = Vec::new(); + for disjunction_complement in disjunctions { + match disjunction_complement { + DisjunctionComplement::Solvables(version_set, solvables) => { + let name_id = self.cache.provider().version_set_name(version_set); + let pending_forbid_clauses = + self.pending_forbid_clauses.entry(name_id).or_default(); + disjunction_literals.reserve(solvables.len()); + pending_forbid_clauses.reserve(solvables.len()); + for &solvable in solvables { + let variable = self.state.variable_map.intern_solvable(solvable); + disjunction_literals.push(variable.positive()); + pending_forbid_clauses.push(variable); + } + } + DisjunctionComplement::Empty(version_set) => { + let name_id = self.cache.provider().version_set_name(version_set); + let at_least_one_of_var = match self + .state + .at_least_one_tracker + .get(&name_id) + .copied() + .or_else(|| self.new_at_least_one_packages.get(&name_id).copied()) + { + Some(variable) => variable, + None => { + let variable = self + .state + .variable_map + .alloc_at_least_one_variable(name_id); + self.new_at_least_one_packages.insert(name_id, variable); + variable + } + }; + disjunction_literals.push(at_least_one_of_var.negative()); + } + } + } + + let disjunction_id = self.state.disjunctions.alloc(Disjunction { + literals: disjunction_literals, + _condition: condition, + }); + conditions.push(Some(disjunction_id)); + } + } else { + conditions.push(None); } - // Add the requirements clause - let no_candidates = candidates.iter().all(|candidates| candidates.is_empty()); - let (watched_literals, conflict, kind) = WatchedLiterals::requires( - variable, - requirement, - version_set_variables.iter().flatten().copied(), - &self.state.decision_tracker, - ); - let clause_id = self.state.clauses.alloc(watched_literals, kind); + for condition in conditions { + // Add the requirements clause + let no_candidates = candidates.iter().all(|candidates| candidates.is_empty()); + let condition_literals = + condition.map(|id| self.state.disjunctions[id].literals.as_slice()); + let (watched_literals, conflict, kind) = WatchedLiterals::requires( + variable, + requirement.requirement, + version_set_variables.iter().flatten().copied(), + condition.zip(condition_literals), + &self.state.decision_tracker, + ); + let clause_id = self.state.clauses.alloc(watched_literals, kind); - let watched_literals = self.state.clauses.watched_literals[clause_id.to_usize()].as_mut(); + let watched_literals = + self.state.clauses.watched_literals[clause_id.to_usize()].as_mut(); + + if let Some(watched_literals) = watched_literals { + self.state + .watches + .start_watching(watched_literals, clause_id); + } - if let Some(watched_literals) = watched_literals { self.state - .watches - .start_watching(watched_literals, clause_id); - } + .requires_clauses + .entry(variable) + .or_default() + .push((requirement.requirement, condition, clause_id)); - self.state - .requires_clauses - .entry(variable) - .or_default() - .push((requirement, clause_id)); - - if conflict { - self.conflicting_clauses.push(clause_id); - } else if no_candidates { - // Add assertions for unit clauses (i.e. those with no matching candidates) - self.state.negative_assertions.push((variable, clause_id)); + if conflict { + self.conflicting_clauses.push(clause_id); + } else if no_candidates && condition.is_none() { + // Add assertions for unit clauses (i.e. those with no matching candidates) + self.state.negative_assertions.push((variable, clause_id)); + } } // Store resolved variables for later self.state .requirement_to_sorted_candidates - .insert(requirement, version_set_variables); + .insert(requirement.requirement, version_set_variables); } /// Called when the candidates for a particular constraint are available. @@ -319,7 +391,7 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { solvable_id, constraint, candidates, - }: ConstraintCandidatesAvailable<'a>, + }: ConstraintCandidatesAvailable<'cache>, ) { tracing::trace!( "non matching candidates available for {} {}", @@ -358,7 +430,8 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { } } - /// Adds clauses to forbid any other clauses than the locked solvable to be installed. + /// Adds clauses to forbid any other clauses than the locked solvable to be + /// installed. fn add_locked_package_clauses( &mut self, locked_solvable_id: SolvableId, @@ -464,20 +537,56 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { /// Enqueues retrieving the candidates for a particular requirement. These /// candidates are already filtered and sorted. - fn queue_requirement(&mut self, solvable_id: SolvableOrRootId, requirement: Requirement) { + fn queue_conditional_requirement( + &mut self, + solvable_id: SolvableOrRootId, + requirement: ConditionalRequirement, + ) { let cache = self.cache; let query_requirements_candidates = async move { - let candidates = - futures::future::try_join_all(requirement.version_sets(cache.provider()).map( - |version_set| cache.get_or_cache_sorted_candidates_for_version_set(version_set), - )) - .await?; + let candidates = futures::future::try_join_all( + requirement + .requirement + .version_sets(cache.provider()) + .map(|version_set| { + cache.get_or_cache_sorted_candidates_for_version_set(version_set) + }), + ); + + let condition_candidates = match requirement.condition { + Some(condition) => futures::future::try_join_all( + conditions::convert_conditions_to_dnf(condition, cache.provider()) + .into_iter() + .map(|cnf| { + futures::future::try_join_all(cnf.into_iter().map(|version_set| { + cache + .get_or_cache_non_matching_candidates(version_set) + .map_ok(move |matching_candidates| { + if matching_candidates.is_empty() { + DisjunctionComplement::Empty(version_set) + } else { + DisjunctionComplement::Solvables( + version_set, + matching_candidates, + ) + } + }) + })) + }), + ) + .map_ok(move |dnf| Some((condition, dnf))) + .left_future(), + None => ready(Ok(None)).right_future(), + }; + + let (candidates, condition) = futures::try_join!(candidates, condition_candidates)?; Ok(TaskResult::RequirementCandidates( RequirementCandidatesAvailable { solvable_id, requirement, candidates, + condition, }, )) }; @@ -506,4 +615,147 @@ impl<'a, D: DependencyProvider> Encoder<'a, D> { self.pending_futures .push(query_constraints_candidates.boxed_local()); } + + /// Add forbid clauses for solvables that we discovered as reachable from a + /// requires clause. + /// + /// The number of forbid clauses for a package is O(n log n) so we only add + /// clauses for the packages that are reachable from a requirement as an + /// optimization. + fn add_pending_forbid_clauses(&mut self) { + for (name_id, candidate_var) in + self.pending_forbid_clauses + .drain(..) + .flat_map(|(name_id, candidate_vars)| { + candidate_vars + .into_iter() + .map(move |candidate_var| (name_id, candidate_var)) + }) + { + // Add forbid constraints for this solvable on all other + // solvables that have been visited already for the same + // version set name. + let other_solvables = self.state.at_most_one_trackers.entry(name_id).or_default(); + let variable_is_new = other_solvables.add( + candidate_var, + |a, b, positive| { + let literal_b = if positive { b.positive() } else { b.negative() }; + let literal_a = a.negative(); + let (watched_literals, kind) = + WatchedLiterals::forbid_multiple(a, literal_b, name_id); + let clause_id = self.state.clauses.alloc(watched_literals, kind); + let watched_literals = self.state.clauses.watched_literals + [clause_id.to_usize()] + .as_mut() + .expect("forbid clause must have watched literals"); + self.state + .watches + .start_watching(watched_literals, clause_id); + + // Add a decision if a decision has already been made for one of the literals. + let set_literal = match ( + literal_a.eval(self.state.decision_tracker.map()), + literal_b.eval(self.state.decision_tracker.map()), + ) { + (Some(false), None) => Some(literal_b), + (None, Some(false)) => Some(literal_a), + (Some(false), Some(false)) => unreachable!( + "both literals cannot be false as that would be a conflict" + ), + _ => None, + }; + if let Some(literal) = set_literal { + self.state + .decision_tracker + .try_add_decision( + Decision::new( + literal.variable(), + literal.satisfying_value(), + clause_id, + ), + self.level, + ) + .expect("we checked that there is no value yet"); + } + }, + || { + self.state + .variable_map + .alloc_forbid_multiple_variable(name_id) + }, + ); + + if variable_is_new { + if let Some(&at_least_one_variable) = self.state.at_least_one_tracker.get(&name_id) + { + let (watched_literals, kind) = + WatchedLiterals::any_of(at_least_one_variable, candidate_var); + let clause_id = self.state.clauses.alloc(watched_literals, kind); + let watched_literals = self.state.clauses.watched_literals + [clause_id.to_usize()] + .as_mut() + .expect("forbid clause must have watched literals"); + self.state + .watches + .start_watching(watched_literals, clause_id); + } + } + } + } + + /// Adds clauses to track if at least one solvable for a particular package + /// is selected. An auxiliary variable is introduced and for each solvable a + /// clause is added that forces the auxiliary variable to turn true if any + /// solvable is selected. + /// + /// This function only looks at solvables that are added to the at most once + /// tracker. The encoder has an optimization that it only creates clauses + /// for packages that are references from a requires clause. Any other + /// solvable will never be selected anyway so we can completely ignore it. + /// + /// No clause is added to force the auxiliary variable to turn false. This + /// is on purpose because we dont not need this state to compute a proper + /// solution. + /// + /// See [`super::conditions`] for more information about conditions. + fn add_pending_at_least_one_clauses(&mut self) { + for (name_id, at_least_one_variable) in self.new_at_least_one_packages.drain(..) { + // Find the at-most-one tracker for the package. We want to reuse the same + // variables. + let variables = self + .state + .at_most_one_trackers + .get(&name_id) + .map(|tracker| &tracker.variables); + + // Add clauses for the existing variables. + for &helper_var in variables.into_iter().flatten() { + let (watched_literals, kind) = + WatchedLiterals::any_of(at_least_one_variable, helper_var); + let clause_id = self.state.clauses.alloc(watched_literals, kind); + let watched_literals = self.state.clauses.watched_literals[clause_id.to_usize()] + .as_mut() + .expect("forbid clause must have watched literals"); + self.state + .watches + .start_watching(watched_literals, clause_id); + + // Assign true if any of the variables is true. + if self.state.decision_tracker.assigned_value(helper_var) == Some(true) { + self.state + .decision_tracker + .try_add_decision( + Decision::new(at_least_one_variable, true, clause_id), + self.level, + ) + .expect("the at least one variable must be undecided"); + } + } + + // Record that we have a variable for this package. + self.state + .at_least_one_tracker + .insert(name_id, at_least_one_variable); + } + } } diff --git a/src/solver/mod.rs b/src/solver/mod.rs index d40dcf22..8e0e25ec 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -1,18 +1,21 @@ +use std::{any::Any, fmt::Display, ops::ControlFlow}; + use ahash::{HashMap, HashSet}; pub use cache::SolverCache; use clause::{Clause, Literal, WatchedLiterals}; +use conditions::{Disjunction, DisjunctionId}; use decision::Decision; use decision_tracker::DecisionTracker; use elsa::FrozenMap; use encoding::Encoder; use indexmap::IndexMap; use itertools::Itertools; -use std::{any::Any, fmt::Display, ops::ControlFlow}; use variable_map::VariableMap; use watch_map::WatchMap; use crate::{ - Dependencies, DependencyProvider, KnownDependencies, Requirement, VersionSetId, + ConditionalRequirement, Dependencies, DependencyProvider, KnownDependencies, Requirement, + VersionSetId, conflict::Conflict, internal::{ arena::{Arena, ArenaId}, @@ -26,6 +29,7 @@ use crate::{ mod binary_encoding; mod cache; pub(crate) mod clause; +mod conditions; mod decision; mod decision_map; mod decision_tracker; @@ -44,7 +48,7 @@ mod watch_map; /// This struct follows the builder pattern and can have its fields set by one /// of the available setter methods. pub struct Problem { - requirements: Vec, + requirements: Vec, constraints: Vec, soft_requirements: S, } @@ -73,7 +77,7 @@ impl> Problem { /// /// Returns the [`Problem`] for further mutation or to pass to /// [`Solver::solve`]. - pub fn requirements(self, requirements: Vec) -> Self { + pub fn requirements(self, requirements: Vec) -> Self { Self { requirements, ..self @@ -157,10 +161,12 @@ pub struct Solver { activity_decay: f32, } +type RequiresClause = (Requirement, Option, ClauseId); + #[derive(Default)] pub(crate) struct SolverState { pub(crate) clauses: Clauses, - requires_clauses: IndexMap, ahash::RandomState>, + requires_clauses: IndexMap, ahash::RandomState>, watches: WatchMap, /// A mapping from requirements to the variables that represent the @@ -176,11 +182,17 @@ pub(crate) struct SolverState { learnt_why: Mapping>, learnt_clause_ids: Vec, + disjunctions: Arena, + clauses_added_for_package: HashSet, clauses_added_for_solvable: HashSet, - forbidden_clauses_added: HashMap>, + at_most_one_trackers: HashMap>, + + /// Keeps track of auxiliary variables that are used to encode at-least-one + /// solvable for a package. + at_least_one_tracker: HashMap, - decision_tracker: DecisionTracker, + pub(crate) decision_tracker: DecisionTracker, /// Activity score per package. name_activity: Vec, @@ -433,7 +445,8 @@ impl Solver { // Add the clauses for the root solvable. let conflicting_clauses = self.async_runtime.block_on( - Encoder::new(&mut self.state, &self.cache, root_deps).encode([root_solvable]), + Encoder::new(&mut self.state, &self.cache, root_deps, level) + .encode([root_solvable]), )?; if let Some(clause_id) = conflicting_clauses.into_iter().next() { @@ -551,7 +564,7 @@ impl Solver { .collect::>(); let conflicting_clauses = self.async_runtime.block_on( - Encoder::new(&mut self.state, &self.cache, root_deps).encode(new_solvables), + Encoder::new(&mut self.state, &self.cache, root_deps, level).encode(new_solvables), )?; // Serially process the outputs, to reduce the need for synchronization @@ -584,7 +597,11 @@ impl Solver { clause_id: ClauseId, ) -> Result { if starting_level == 0 { - tracing::trace!("Unsolvable: {:?}", clause_id); + tracing::trace!( + "Unsolvable: {}", + self.state.clauses.kinds[clause_id.to_usize()] + .display(&self.state.variable_map, self.provider(),) + ); Err(UnsolvableOrCancelled::Unsolvable( self.analyze_unsolvable(clause_id), )) @@ -708,9 +725,21 @@ impl Solver { continue; } - for (deps, clause_id) in requirements.iter() { + for (deps, condition, clause_id) in requirements.iter() { let mut candidate = ControlFlow::Break(()); + // If the clause has a condition that is not yet satisfied we need to skip it. + if let Some(condition) = *condition { + let literals = &self.state.disjunctions[condition].literals; + if !literals.iter().all(|c| { + let value = c.eval(self.state.decision_tracker.map()); + value == Some(false) + }) { + // The condition is not satisfied, skip this clause. + continue; + } + } + // Get the candidates for the individual version sets. let version_set_candidates = &self.state.requirement_to_sorted_candidates[deps]; @@ -1074,6 +1103,7 @@ impl Solver { clause, &self.state.learnt_clauses, &self.state.requirement_to_sorted_candidates, + &self.state.disjunctions, self.state.decision_tracker.map(), watch_index, ) { @@ -1237,6 +1267,7 @@ impl Solver { self.state.clauses.kinds[clause_id.to_usize()].visit_literals( &self.state.learnt_clauses, &self.state.requirement_to_sorted_candidates, + &self.state.disjunctions, |literal| { involved.insert(literal.variable()); }, @@ -1275,6 +1306,7 @@ impl Solver { self.state.clauses.kinds[why.to_usize()].visit_literals( &self.state.learnt_clauses, &self.state.requirement_to_sorted_candidates, + &self.state.disjunctions, |literal| { if literal.eval(self.state.decision_tracker.map()) == Some(true) { assert_eq!(literal.variable(), decision.variable); @@ -1322,6 +1354,7 @@ impl Solver { clause_kinds[clause_id.to_usize()].visit_literals( &self.state.learnt_clauses, &self.state.requirement_to_sorted_candidates, + &self.state.disjunctions, |literal| { if !first_iteration && literal.variable() == conflicting_solvable { // We are only interested in the causes of the conflict, so we ignore the diff --git a/src/solver/variable_map.rs b/src/solver/variable_map.rs index d21bffee..f4fc9671 100644 --- a/src/solver/variable_map.rs +++ b/src/solver/variable_map.rs @@ -38,6 +38,10 @@ pub enum VariableOrigin { /// A variable that helps encode an at most one constraint. ForbidMultiple(NameId), + + /// A variable that indicates that any solvable of a particular package is + /// part of the solution. + AtLeastOne(NameId), } impl Default for VariableMap { @@ -99,6 +103,17 @@ impl VariableMap { variable_id } + /// Allocate a variable helps encode whether at least one solvable for a + /// particular package is selected. + pub fn alloc_at_least_one_variable(&mut self, name: NameId) -> VariableId { + let id = self.next_id; + self.next_id += 1; + let variable_id = VariableId::from_usize(id); + self.origins + .insert(variable_id, VariableOrigin::AtLeastOne(name)); + variable_id + } + /// Returns the origin of a variable. The origin describes the semantics of /// a variable. pub fn origin(&self, variable_id: VariableId) -> VariableOrigin { @@ -147,6 +162,9 @@ impl Display for VariableDisplay<'_, I> { VariableOrigin::ForbidMultiple(name) => { write!(f, "forbid-multiple({})", self.interner.display_name(name)) } + VariableOrigin::AtLeastOne(name) => { + write!(f, "any-of({})", self.interner.display_name(name)) + } } } } diff --git a/src/utils/pool.rs b/src/utils/pool.rs index f660f200..f4dc9542 100644 --- a/src/utils/pool.rs +++ b/src/utils/pool.rs @@ -3,11 +3,14 @@ use std::{ hash::Hash, }; -use crate::internal::{ - arena::Arena, - frozen_copy_map::FrozenCopyMap, - id::{NameId, SolvableId, StringId, VersionSetId, VersionSetUnionId}, - small_vec::SmallVec, +use crate::{ + Condition, ConditionId, + internal::{ + arena::Arena, + frozen_copy_map::FrozenCopyMap, + id::{NameId, SolvableId, StringId, VersionSetId, VersionSetUnionId}, + small_vec::SmallVec, + }, }; /// A solvable represents a single candidate of a package. @@ -50,6 +53,12 @@ pub struct Pool { version_set_to_id: FrozenCopyMap<(NameId, VS), VersionSetId, ahash::RandomState>, version_set_unions: Arena>, + + /// Conditions that can be used to filter solvables. + conditions: Arena, + + /// Map from condition to its id + condition_to_id: FrozenCopyMap, } impl Default for Pool { @@ -65,6 +74,8 @@ impl Default for Pool { version_set_to_id: Default::default(), version_sets: Arena::new(), version_set_unions: Arena::new(), + conditions: Arena::new(), + condition_to_id: Default::default(), } } } @@ -213,6 +224,23 @@ impl Pool { ) -> impl Iterator + '_ { self.version_set_unions[id].iter().copied() } + + /// Resolve the condition associated with the provided id. + pub fn resolve_condition(&self, id: ConditionId) -> &Condition { + &self.conditions[id] + } + + /// Interns a condition into the pool and returns its `ConditionId`. + /// Conditions are deduplicated, so if the same condition is inserted + /// twice the same `ConditionId` will be returned. + pub fn intern_condition(&self, condition: Condition) -> ConditionId { + if let Some(id) = self.condition_to_id.get_copy(&condition) { + return id; + } + let id = self.conditions.alloc(condition.clone()); + self.condition_to_id.insert_copy(condition, id); + id + } } /// A helper struct to visualize a name. diff --git a/tests/snapshots/solver__root_constraints.snap b/tests/snapshots/solver__root_constraints.snap deleted file mode 100644 index 160d78c9..00000000 --- a/tests/snapshots/solver__root_constraints.snap +++ /dev/null @@ -1,8 +0,0 @@ ---- -source: tests/solver.rs -expression: "solve_for_snapshot(snapshot_provider, &[union_req], &[union_constraint])" ---- -The following packages are incompatible -└─ union * can be installed with any of the following options: - └─ union union=1 -├─ the constraint union 5 cannot be fulfilled diff --git a/tests/solver/bundle_box/conditional_spec.rs b/tests/solver/bundle_box/conditional_spec.rs new file mode 100644 index 00000000..00b1e00d --- /dev/null +++ b/tests/solver/bundle_box/conditional_spec.rs @@ -0,0 +1,21 @@ +use super::Spec; +use chumsky::{Parser, error}; +use resolvo::LogicalOperator; + +#[derive(Debug, Clone)] +pub struct ConditionalSpec { + pub condition: Option, + pub specs: Vec, +} + +impl ConditionalSpec { + pub fn from_str(s: &str) -> Result>> { + super::parser::conditional_spec().parse(s).into_result() + } +} + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum SpecCondition { + Binary(LogicalOperator, Box<[SpecCondition; 2]>), + Requirement(Spec), +} diff --git a/tests/solver/bundle_box/mod.rs b/tests/solver/bundle_box/mod.rs new file mode 100644 index 00000000..33ccd6b4 --- /dev/null +++ b/tests/solver/bundle_box/mod.rs @@ -0,0 +1,431 @@ +// Let's define our own packaging version system and dependency specification. +// This is a very simple version system, where a package is identified by a name +// and a version in which the version is just an integer. The version is a range +// so can be noted as 0..2 or something of the sorts, we also support constrains +// which means it should not use that package version this is also represented +// with a range. +// +// You can also use just a single number for a range like `package 0` which +// means the range from 0..1 (excluding the end) +// +// Lets call the tuples of (Name, Version) a `Pack` and the tuples of (Name, +// Ranges) a `Spec` +// +// We also need to create a custom provider that tells us how to sort the +// candidates. This is unique to each packaging ecosystem. Let's call our +// ecosystem 'BundleBox' so that how we call the provider as well. + +mod conditional_spec; +mod pack; +pub mod parser; +mod spec; + +use std::{ + any::Any, + cell::{Cell, RefCell}, + collections::HashSet, + fmt::Display, + rc::Rc, + sync::{ + Arc, + atomic::{AtomicUsize, Ordering}, + }, + time::Duration, +}; + +use ahash::HashMap; +pub use conditional_spec::{ConditionalSpec, SpecCondition}; +use indexmap::IndexMap; +use itertools::Itertools; +pub use pack::Pack; +use resolvo::{ + Candidates, Condition, ConditionId, ConditionalRequirement, Dependencies, DependencyProvider, + Interner, KnownDependencies, NameId, SolvableId, SolverCache, StringId, VersionSetId, + VersionSetUnionId, snapshot::DependencySnapshot, utils::Pool, +}; +pub use spec::Spec; +use version_ranges::Ranges; + +/// This provides sorting functionality for our `BundleBox` packaging system +#[derive(Default)] +pub struct BundleBoxProvider { + pub pool: Pool>, + id_to_condition: Vec, + conditions: HashMap, + packages: IndexMap>, + favored: HashMap, + locked: HashMap, + excluded: HashMap>, + cancel_solving: Cell, + // TODO: simplify? + concurrent_requests: Arc, + pub concurrent_requests_max: Rc>, + pub sleep_before_return: bool, + + // A mapping of packages that we have requested candidates for. This way we can keep track of + // duplicate requests. + requested_candidates: RefCell>, + requested_dependencies: RefCell>, + interned_solvables: RefCell>, +} + +#[derive(Debug, Clone)] +struct BundleBoxPackageDependencies { + dependencies: Vec, + constrains: Vec, +} + +impl BundleBoxProvider { + pub fn new() -> Self { + Default::default() + } + + pub fn package_name(&self, name: &str) -> NameId { + self.pool + .lookup_package_name(&name.to_string()) + .expect("package missing") + } + + pub fn intern_condition(&mut self, condition: &SpecCondition) -> ConditionId { + if let Some(id) = self.conditions.get(&condition) { + return *id; + } + + if let SpecCondition::Binary(_op, sides) = condition { + self.intern_condition(&sides[0]); + self.intern_condition(&sides[1]); + } + + let id = ConditionId::new(self.id_to_condition.len() as u32); + self.id_to_condition.push(condition.clone()); + self.conditions.insert(condition.clone(), id); + id + } + + pub fn requirements(&mut self, requirements: &[&str]) -> Vec { + requirements + .iter() + .map(|dep| ConditionalSpec::from_str(*dep).unwrap()) + .map(|spec| { + let mut iter = spec + .specs + .into_iter() + .map(|spec| self.intern_version_set(&spec)) + .peekable(); + let first = iter.next().unwrap(); + let requirement = if iter.peek().is_some() { + self.pool.intern_version_set_union(first, iter).into() + } else { + first.into() + }; + + let condition = spec.condition.map(|c| self.intern_condition(&c)); + + ConditionalRequirement { + condition, + requirement, + } + }) + .collect() + } + + pub fn version_sets(&mut self, requirements: &[&str]) -> Vec { + requirements + .iter() + .map(|dep| Spec::from_str(*dep).unwrap()) + .map(|spec| { + let name = self.pool.intern_package_name(&spec.name); + self.pool.intern_version_set(name, spec.versions) + }) + .collect() + } + + pub fn intern_version_set(&self, spec: &Spec) -> VersionSetId { + let dep_name = self.pool.intern_package_name(&spec.name); + self.pool + .intern_version_set(dep_name, spec.versions.clone()) + } + + pub fn from_packages(packages: &[(&str, u32, Vec<&str>)]) -> Self { + let mut result = Self::new(); + for (name, version, deps) in packages { + result.add_package(name, Pack::new(*version), deps, &[]); + } + result + } + + pub fn set_favored(&mut self, package_name: &str, version: u32) { + self.favored + .insert(package_name.to_owned(), Pack::new(version)); + } + + pub fn exclude(&mut self, package_name: &str, version: u32, reason: impl Into) { + self.excluded + .entry(package_name.to_owned()) + .or_default() + .insert(Pack::new(version), reason.into()); + } + + pub fn set_locked(&mut self, package_name: &str, version: u32) { + self.locked + .insert(package_name.to_owned(), Pack::new(version)); + } + + pub fn add_package( + &mut self, + package_name: &str, + package_version: Pack, + dependencies: &[&str], + constrains: &[&str], + ) { + self.pool.intern_package_name(package_name); + + let dependencies = self.requirements(dependencies); + + let constrains = constrains + .iter() + .map(|dep| Spec::from_str(dep)) + .collect::, _>>() + .unwrap(); + + self.packages + .entry(package_name.to_owned()) + .or_default() + .insert( + package_version, + BundleBoxPackageDependencies { + dependencies, + constrains, + }, + ); + } + + // Sends a value from the dependency provider to the solver, introducing a + // minimal delay to force concurrency to be used (unless there is no async + // runtime available) + async fn maybe_delay(&self, value: T) -> T { + if self.sleep_before_return { + tokio::time::sleep(Duration::from_millis(10)).await; + self.concurrent_requests.fetch_sub(1, Ordering::SeqCst); + value + } else { + value + } + } + + pub fn into_snapshot(self) -> DependencySnapshot { + let name_ids = self + .packages + .keys() + .filter_map(|name| self.pool.lookup_package_name(name)) + .collect::>(); + DependencySnapshot::from_provider(self, name_ids, [], []).unwrap() + } + + pub fn intern_solvable(&self, name_id: NameId, pack: Pack) -> SolvableId { + *self + .interned_solvables + .borrow_mut() + .entry((name_id, pack)) + .or_insert_with_key(|&(name_id, pack)| self.pool.intern_solvable(name_id, pack)) + } + + pub fn solvable_id(&self, name: impl Into, version: impl Into) -> SolvableId { + self.intern_solvable(self.pool.intern_package_name(name.into()), version.into()) + } +} + +impl Interner for BundleBoxProvider { + fn display_solvable(&self, solvable: SolvableId) -> impl Display + '_ { + let solvable = self.pool.resolve_solvable(solvable); + format!("{}={}", self.display_name(solvable.name), solvable.record) + } + + fn display_merged_solvables(&self, solvables: &[SolvableId]) -> impl Display + '_ { + if solvables.is_empty() { + return "".to_string(); + } + + let name = self.display_name(self.pool.resolve_solvable(solvables[0]).name); + let versions = solvables + .iter() + .map(|&s| self.pool.resolve_solvable(s).record.version) + .sorted(); + format!("{name} {}", versions.format(" | ")) + } + + fn display_name(&self, name: NameId) -> impl Display + '_ { + self.pool.resolve_package_name(name).clone() + } + + fn display_version_set(&self, version_set: VersionSetId) -> impl Display + '_ { + self.pool.resolve_version_set(version_set).clone() + } + + fn display_string(&self, string_id: StringId) -> impl Display + '_ { + self.pool.resolve_string(string_id).to_owned() + } + + fn version_set_name(&self, version_set: VersionSetId) -> NameId { + self.pool.resolve_version_set_package_name(version_set) + } + + fn solvable_name(&self, solvable: SolvableId) -> NameId { + self.pool.resolve_solvable(solvable).name + } + fn version_sets_in_union( + &self, + version_set_union: VersionSetUnionId, + ) -> impl Iterator { + self.pool.resolve_version_set_union(version_set_union) + } + + fn resolve_condition(&self, condition: ConditionId) -> Condition { + let condition = condition.as_u32(); + let condition = &self.id_to_condition[condition as usize]; + match condition { + SpecCondition::Binary(op, items) => Condition::Binary( + *op, + *self.conditions.get(&items[0]).unwrap(), + *self.conditions.get(&items[1]).unwrap(), + ), + SpecCondition::Requirement(requirement) => { + Condition::Requirement(self.intern_version_set(requirement)) + } + } + } +} + +impl DependencyProvider for BundleBoxProvider { + async fn filter_candidates( + &self, + candidates: &[SolvableId], + version_set: VersionSetId, + inverse: bool, + ) -> Vec { + let range = self.pool.resolve_version_set(version_set); + candidates + .iter() + .copied() + .filter(|s| range.contains(&self.pool.resolve_solvable(*s).record) == !inverse) + .collect() + } + + async fn sort_candidates(&self, _solver: &SolverCache, solvables: &mut [SolvableId]) { + solvables.sort_by(|a, b| { + let a = self.pool.resolve_solvable(*a).record; + let b = self.pool.resolve_solvable(*b).record; + // We want to sort with highest version on top + b.version.cmp(&a.version) + }); + } + + async fn get_candidates(&self, name: NameId) -> Option { + let concurrent_requests = self.concurrent_requests.fetch_add(1, Ordering::SeqCst); + self.concurrent_requests_max.set( + self.concurrent_requests_max + .get() + .max(concurrent_requests + 1), + ); + + assert!( + self.requested_candidates.borrow_mut().insert(name), + "duplicate get_candidates request" + ); + + let package_name = self.pool.resolve_package_name(name); + let Some(package) = self.packages.get(package_name) else { + return self.maybe_delay(None).await; + }; + + let mut candidates = Candidates { + candidates: Vec::with_capacity(package.len()), + ..Candidates::default() + }; + let favor = self.favored.get(package_name); + let locked = self.locked.get(package_name); + let excluded = self.excluded.get(package_name); + for pack in package.keys() { + let solvable = self.intern_solvable(name, *pack); + candidates.candidates.push(solvable); + if Some(pack) == favor { + candidates.favored = Some(solvable); + } + if Some(pack) == locked { + candidates.locked = Some(solvable); + } + if let Some(excluded) = excluded.and_then(|d| d.get(pack)) { + candidates + .excluded + .push((solvable, self.pool.intern_string(excluded))); + } + } + + self.maybe_delay(Some(candidates)).await + } + + async fn get_dependencies(&self, solvable: SolvableId) -> Dependencies { + tracing::info!( + "get dependencies for {}", + self.pool + .resolve_solvable(solvable) + .name + .display(&self.pool) + ); + + let concurrent_requests = self.concurrent_requests.fetch_add(1, Ordering::SeqCst); + self.concurrent_requests_max.set( + self.concurrent_requests_max + .get() + .max(concurrent_requests + 1), + ); + + assert!( + self.requested_dependencies.borrow_mut().insert(solvable), + "duplicate get_dependencies request" + ); + + let candidate = self.pool.resolve_solvable(solvable); + let package_name = self.pool.resolve_package_name(candidate.name); + let pack = candidate.record; + + if pack.cancel_during_get_dependencies { + self.cancel_solving.set(true); + let reason = self.pool.intern_string("cancelled"); + return self.maybe_delay(Dependencies::Unknown(reason)).await; + } + + if pack.unknown_deps { + let reason = self.pool.intern_string("could not retrieve deps"); + return self.maybe_delay(Dependencies::Unknown(reason)).await; + } + + let Some(deps) = self.packages.get(package_name).and_then(|v| v.get(&pack)) else { + return self + .maybe_delay(Dependencies::Known(Default::default())) + .await; + }; + + let mut result = KnownDependencies { + requirements: Vec::with_capacity(deps.dependencies.len()), + constrains: Vec::with_capacity(deps.constrains.len()), + }; + result.requirements = deps.dependencies.clone(); + + for req in &deps.constrains { + let dep_name = self.pool.intern_package_name(&req.name); + let dep_spec = self.pool.intern_version_set(dep_name, req.versions.clone()); + result.constrains.push(dep_spec); + } + + self.maybe_delay(Dependencies::Known(result)).await + } + + fn should_cancel_with_value(&self) -> Option> { + if self.cancel_solving.get() { + Some(Box::new("cancelled!".to_string())) + } else { + None + } + } +} diff --git a/tests/solver/bundle_box/pack.rs b/tests/solver/bundle_box/pack.rs new file mode 100644 index 00000000..1cf3c873 --- /dev/null +++ b/tests/solver/bundle_box/pack.rs @@ -0,0 +1,57 @@ +use std::fmt::{Display, Formatter}; +use std::str::FromStr; + +/// This is `Pack` which is a unique version and name in our bespoke packaging +/// system +#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Hash)] +pub struct Pack { + pub version: u32, + pub unknown_deps: bool, + pub cancel_during_get_dependencies: bool, +} + +impl Pack { + pub fn new(version: u32) -> Pack { + Pack { + version, + unknown_deps: false, + cancel_during_get_dependencies: false, + } + } + + pub fn with_unknown_deps(mut self) -> Pack { + self.unknown_deps = true; + self + } + + pub fn cancel_during_get_dependencies(mut self) -> Pack { + self.cancel_during_get_dependencies = true; + self + } +} + +impl From for Pack { + fn from(value: u32) -> Self { + Pack::new(value) + } +} + +impl From for Pack { + fn from(value: i32) -> Self { + Pack::new(value as u32) + } +} + +impl Display for Pack { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.version) + } +} + +impl FromStr for Pack { + type Err = std::num::ParseIntError; + + fn from_str(s: &str) -> Result { + u32::from_str(s).map(Pack::new) + } +} diff --git a/tests/solver/bundle_box/parser.rs b/tests/solver/bundle_box/parser.rs new file mode 100644 index 00000000..6e3d6985 --- /dev/null +++ b/tests/solver/bundle_box/parser.rs @@ -0,0 +1,119 @@ +use super::{ConditionalSpec, Pack, Spec, SpecCondition}; +use chumsky::{ + IterParser, Parser, error, + error::LabelError, + extra, + extra::ParserExtra, + input::{SliceInput, StrInput}, + pratt::{infix, left}, + prelude::{any, just}, + text, + text::{Char, TextExpected}, + util::MaybeRef, +}; +use resolvo::LogicalOperator; +use version_ranges::Ranges; + +/// Parses a package name identifier. +pub fn name<'src, I, E>() -> impl Parser<'src, I, >::Slice, E> + Copy +where + I: StrInput<'src>, + I::Token: Char + 'src, + E: ParserExtra<'src, I>, + E::Error: LabelError<'src, I, TextExpected<'src, I>>, +{ + any() + .try_map(|c: I::Token, span| { + if c.to_ascii() + .map(|i| i.is_ascii_alphabetic() || i == b'_') + .unwrap_or(false) + { + Ok(c) + } else { + Err(LabelError::expected_found( + [TextExpected::IdentifierPart], + Some(MaybeRef::Val(c)), + span, + )) + } + }) + .then( + any() + .try_map(|c: I::Token, span| { + if c.to_ascii().map_or(false, |i| { + i.is_ascii_alphanumeric() || i == b'_' || i == b'-' + }) { + Ok(()) + } else { + Err(LabelError::expected_found( + [TextExpected::IdentifierPart], + Some(MaybeRef::Val(c)), + span, + )) + } + }) + .repeated(), + ) + .to_slice() +} + +/// Parses a range of package versions. E.g. `5` or `1..5`. +fn ranges<'src>() +-> impl Parser<'src, &'src str, Ranges, extra::Err>> { + text::int(10) + .map(|s: &str| s.parse().unwrap()) + .then( + just("..") + .padded() + .ignore_then(text::int(10).map(|s: &str| s.parse().unwrap()).padded()) + .or_not(), + ) + .map(|(left, right)| { + let right = Pack::new(right.unwrap_or_else(|| left + 1)); + Ranges::between(Pack::new(left), right) + }) +} + +/// Parses a single [`Spec`]. E.g. `foo 1..2` or `bar 3` or `baz`. +pub(crate) fn spec<'src>() +-> impl Parser<'src, &'src str, Spec, extra::Err>> { + name() + .padded() + .then(ranges().or_not()) + .map(|(name, range)| Spec::new(name.to_string(), range.unwrap_or(Ranges::full()))) +} + +fn condition<'src>() +-> impl Parser<'src, &'src str, SpecCondition, extra::Err>> { + let and = just("and").padded().map(|_| LogicalOperator::And); + let or = just("or").padded().map(|_| LogicalOperator::Or); + + let single = spec().map(SpecCondition::Requirement); + + single.pratt(( + infix(left(1), and, |lhs, op, rhs, _| { + SpecCondition::Binary(op, Box::new([lhs, rhs])) + }), + infix(left(1), or, |lhs, op, rhs, _| { + SpecCondition::Binary(op, Box::new([lhs, rhs])) + }), + )) +} + +pub(crate) fn union_spec<'src>() +-> impl Parser<'src, &'src str, Vec, extra::Err>> { + spec() + .separated_by(just("|").padded()) + .at_least(1) + .collect() +} + +pub(crate) fn conditional_spec<'src>() +-> impl Parser<'src, &'src str, ConditionalSpec, extra::Err>> { + union_spec() + .then(just("; if").padded().ignore_then(condition()).or_not()) + .map(|(spec, condition)| ConditionalSpec { + condition, + specs: spec, + }) +} diff --git a/tests/solver/bundle_box/spec.rs b/tests/solver/bundle_box/spec.rs new file mode 100644 index 00000000..cf22ed27 --- /dev/null +++ b/tests/solver/bundle_box/spec.rs @@ -0,0 +1,23 @@ +use super::Pack; +use chumsky::{Parser, error}; +use version_ranges::Ranges; + +/// We can use this to see if a `Pack` is contained in a range of package +/// versions or a `Spec` +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Spec { + pub name: String, + pub versions: Ranges, +} + +impl Spec { + pub fn new(name: String, versions: Ranges) -> Self { + Self { name, versions } + } +} + +impl Spec { + pub fn from_str(s: &str) -> Result>> { + super::parser::spec().parse(s).into_result() + } +} diff --git a/tests/solver.rs b/tests/solver/main.rs similarity index 58% rename from tests/solver.rs rename to tests/solver/main.rs index 31fb2577..ec8b32f6 100644 --- a/tests/solver.rs +++ b/tests/solver/main.rs @@ -1,539 +1,15 @@ -use std::{ - any::Any, - borrow::Borrow, - cell::{Cell, RefCell}, - collections::HashSet, - fmt::{Debug, Display, Formatter}, - io::{Write, stderr}, - num::ParseIntError, - rc::Rc, - str::FromStr, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, - time::Duration, -}; +mod bundle_box; + +use std::io::{Write, stderr}; -use ahash::HashMap; -use indexmap::IndexMap; +use bundle_box::{BundleBoxProvider, Pack}; use insta::assert_snapshot; use itertools::Itertools; use resolvo::{ - Candidates, Dependencies, DependencyProvider, Interner, KnownDependencies, NameId, Problem, - Requirement, SolvableId, Solver, SolverCache, StringId, UnsolvableOrCancelled, VersionSetId, - VersionSetUnionId, - snapshot::{DependencySnapshot, SnapshotProvider}, - utils::Pool, + ConditionalRequirement, DependencyProvider, Interner, Problem, SolvableId, Solver, + UnsolvableOrCancelled, VersionSetId, }; use tracing_test::traced_test; -use version_ranges::Ranges; - -// Let's define our own packaging version system and dependency specification. -// This is a very simple version system, where a package is identified by a name -// and a version in which the version is just an integer. The version is a range -// so can be noted as 0..2 or something of the sorts, we also support constrains -// which means it should not use that package version this is also represented -// with a range. -// -// You can also use just a single number for a range like `package 0` which -// means the range from 0..1 (excluding the end) -// -// Lets call the tuples of (Name, Version) a `Pack` and the tuples of (Name, -// Ranges) a `Spec` -// -// We also need to create a custom provider that tells us how to sort the -// candidates. This is unique to each packaging ecosystem. Let's call our -// ecosystem 'BundleBox' so that how we call the provider as well. - -/// This is `Pack` which is a unique version and name in our bespoke packaging -/// system -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Copy, Clone, Hash)] -struct Pack { - version: u32, - unknown_deps: bool, - cancel_during_get_dependencies: bool, -} - -impl Pack { - fn new(version: u32) -> Pack { - Pack { - version, - unknown_deps: false, - cancel_during_get_dependencies: false, - } - } - - fn with_unknown_deps(mut self) -> Pack { - self.unknown_deps = true; - self - } - - fn cancel_during_get_dependencies(mut self) -> Pack { - self.cancel_during_get_dependencies = true; - self - } - - fn offset(&self, version_offset: i32) -> Pack { - let mut pack = *self; - pack.version = pack.version.wrapping_add_signed(version_offset); - pack - } -} - -impl From for Pack { - fn from(value: u32) -> Self { - Pack::new(value) - } -} - -impl From for Pack { - fn from(value: i32) -> Self { - Pack::new(value as u32) - } -} - -impl Display for Pack { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.version) - } -} - -impl FromStr for Pack { - type Err = ParseIntError; - - fn from_str(s: &str) -> Result { - u32::from_str(s).map(Pack::new) - } -} - -/// We can use this to see if a `Pack` is contained in a range of package -/// versions or a `Spec` -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -struct Spec { - name: String, - versions: Ranges, -} - -impl Spec { - pub fn new(name: String, versions: Ranges) -> Self { - Self { name, versions } - } - - pub fn parse_union( - spec: &str, - ) -> impl Iterator::Err>> + '_ { - spec.split('|').map(str::trim).map(Spec::from_str) - } -} - -impl FromStr for Spec { - type Err = (); - - fn from_str(s: &str) -> Result { - let split = s.split(' ').collect::>(); - let name = split - .first() - .expect("spec does not have a name") - .to_string(); - - fn version_range(s: Option<&&str>) -> Ranges { - if let Some(s) = s { - let (start, end) = s - .split_once("..") - .map_or((*s, None), |(start, end)| (start, Some(end))); - let start: Pack = start.parse().unwrap(); - let end = end - .map(FromStr::from_str) - .transpose() - .unwrap() - .unwrap_or(start.offset(1)); - Ranges::between(start, end) - } else { - Ranges::full() - } - } - - let versions = version_range(split.get(1)); - - Ok(Spec::new(name, versions)) - } -} - -/// This provides sorting functionality for our `BundleBox` packaging system -#[derive(Default)] -struct BundleBoxProvider { - pool: Pool>, - packages: IndexMap>, - favored: HashMap, - locked: HashMap, - excluded: HashMap>, - cancel_solving: Cell, - // TODO: simplify? - concurrent_requests: Arc, - concurrent_requests_max: Rc>, - sleep_before_return: bool, - - // A mapping of packages that we have requested candidates for. This way we can keep track of - // duplicate requests. - requested_candidates: RefCell>, - requested_dependencies: RefCell>, - interned_solvables: RefCell>, -} - -#[derive(Debug, Clone)] -struct BundleBoxPackageDependencies { - dependencies: Vec>, - constrains: Vec, -} - -impl BundleBoxProvider { - pub fn new() -> Self { - Default::default() - } - - pub fn package_name(&self, name: &str) -> NameId { - self.pool - .lookup_package_name(&name.to_string()) - .expect("package missing") - } - - pub fn requirements>(&self, requirements: &[&str]) -> Vec { - requirements - .iter() - .map(|dep| Spec::from_str(dep).unwrap()) - .map(|spec| self.intern_version_set(&spec)) - .map(From::from) - .collect() - } - - pub fn parse_requirements(&self, requirements: &[&str]) -> Vec { - requirements - .iter() - .map(|deps| { - let specs = Spec::parse_union(deps).map(Result::unwrap); - self.intern_version_set_union(specs).into() - }) - .collect() - } - - pub fn intern_version_set(&self, spec: &Spec) -> VersionSetId { - let dep_name = self.pool.intern_package_name(&spec.name); - self.pool - .intern_version_set(dep_name, spec.versions.clone()) - } - - pub fn intern_version_set_union( - &self, - specs: impl IntoIterator>, - ) -> VersionSetUnionId { - let mut specs = specs - .into_iter() - .map(|spec| self.intern_version_set(spec.borrow())); - self.pool - .intern_version_set_union(specs.next().unwrap(), specs) - } - - pub fn from_packages(packages: &[(&str, u32, Vec<&str>)]) -> Self { - let mut result = Self::new(); - for (name, version, deps) in packages { - result.add_package(name, Pack::new(*version), deps, &[]); - } - result - } - - pub fn set_favored(&mut self, package_name: &str, version: u32) { - self.favored - .insert(package_name.to_owned(), Pack::new(version)); - } - - pub fn exclude(&mut self, package_name: &str, version: u32, reason: impl Into) { - self.excluded - .entry(package_name.to_owned()) - .or_default() - .insert(Pack::new(version), reason.into()); - } - - pub fn set_locked(&mut self, package_name: &str, version: u32) { - self.locked - .insert(package_name.to_owned(), Pack::new(version)); - } - - pub fn add_package( - &mut self, - package_name: &str, - package_version: Pack, - dependencies: &[&str], - constrains: &[&str], - ) { - self.pool.intern_package_name(package_name); - - let dependencies = dependencies - .iter() - .map(|dep| Spec::parse_union(dep).collect()) - .collect::, _>>() - .unwrap(); - - let constrains = constrains - .iter() - .map(|dep| Spec::from_str(dep)) - .collect::, _>>() - .unwrap(); - - self.packages - .entry(package_name.to_owned()) - .or_default() - .insert( - package_version, - BundleBoxPackageDependencies { - dependencies, - constrains, - }, - ); - } - - // Sends a value from the dependency provider to the solver, introducing a - // minimal delay to force concurrency to be used (unless there is no async - // runtime available) - async fn maybe_delay(&self, value: T) -> T { - if self.sleep_before_return { - tokio::time::sleep(Duration::from_millis(10)).await; - self.concurrent_requests.fetch_sub(1, Ordering::SeqCst); - value - } else { - value - } - } - - pub fn into_snapshot(self) -> DependencySnapshot { - let name_ids = self - .packages - .keys() - .filter_map(|name| self.pool.lookup_package_name(name)) - .collect::>(); - DependencySnapshot::from_provider(self, name_ids, [], []).unwrap() - } - - pub fn intern_solvable(&self, name_id: NameId, pack: Pack) -> SolvableId { - *self - .interned_solvables - .borrow_mut() - .entry((name_id, pack)) - .or_insert_with_key(|&(name_id, pack)| self.pool.intern_solvable(name_id, pack)) - } - - pub fn solvable_id(&self, name: impl Into, version: impl Into) -> SolvableId { - self.intern_solvable(self.pool.intern_package_name(name.into()), version.into()) - } -} - -impl Interner for BundleBoxProvider { - fn display_solvable(&self, solvable: SolvableId) -> impl Display + '_ { - let solvable = self.pool.resolve_solvable(solvable); - format!("{}={}", self.display_name(solvable.name), solvable.record) - } - - fn display_merged_solvables(&self, solvables: &[SolvableId]) -> impl Display + '_ { - if solvables.is_empty() { - return "".to_string(); - } - - let name = self.display_name(self.pool.resolve_solvable(solvables[0]).name); - let versions = solvables - .iter() - .map(|&s| self.pool.resolve_solvable(s).record.version) - .sorted(); - format!("{name} {}", versions.format(" | ")) - } - - fn display_name(&self, name: NameId) -> impl Display + '_ { - self.pool.resolve_package_name(name).clone() - } - - fn display_version_set(&self, version_set: VersionSetId) -> impl Display + '_ { - self.pool.resolve_version_set(version_set).clone() - } - - fn display_string(&self, string_id: StringId) -> impl Display + '_ { - self.pool.resolve_string(string_id).to_owned() - } - - fn version_set_name(&self, version_set: VersionSetId) -> NameId { - self.pool.resolve_version_set_package_name(version_set) - } - - fn solvable_name(&self, solvable: SolvableId) -> NameId { - self.pool.resolve_solvable(solvable).name - } - fn version_sets_in_union( - &self, - version_set_union: VersionSetUnionId, - ) -> impl Iterator { - self.pool.resolve_version_set_union(version_set_union) - } -} - -impl DependencyProvider for BundleBoxProvider { - async fn filter_candidates( - &self, - candidates: &[SolvableId], - version_set: VersionSetId, - inverse: bool, - ) -> Vec { - let range = self.pool.resolve_version_set(version_set); - candidates - .iter() - .copied() - .filter(|s| range.contains(&self.pool.resolve_solvable(*s).record) != inverse) - .collect() - } - - async fn sort_candidates(&self, _solver: &SolverCache, solvables: &mut [SolvableId]) { - solvables.sort_by(|a, b| { - let a = self.pool.resolve_solvable(*a).record; - let b = self.pool.resolve_solvable(*b).record; - // We want to sort with highest version on top - b.version.cmp(&a.version) - }); - } - - async fn get_candidates(&self, name: NameId) -> Option { - let concurrent_requests = self.concurrent_requests.fetch_add(1, Ordering::SeqCst); - self.concurrent_requests_max.set( - self.concurrent_requests_max - .get() - .max(concurrent_requests + 1), - ); - - assert!( - self.requested_candidates.borrow_mut().insert(name), - "duplicate get_candidates request" - ); - - let package_name = self.pool.resolve_package_name(name); - let Some(package) = self.packages.get(package_name) else { - return self.maybe_delay(None).await; - }; - - let mut candidates = Candidates { - candidates: Vec::with_capacity(package.len()), - ..Candidates::default() - }; - let favor = self.favored.get(package_name); - let locked = self.locked.get(package_name); - let excluded = self.excluded.get(package_name); - for pack in package.keys() { - let solvable = self.intern_solvable(name, *pack); - candidates.candidates.push(solvable); - if Some(pack) == favor { - candidates.favored = Some(solvable); - } - if Some(pack) == locked { - candidates.locked = Some(solvable); - } - if let Some(excluded) = excluded.and_then(|d| d.get(pack)) { - candidates - .excluded - .push((solvable, self.pool.intern_string(excluded))); - } - } - - self.maybe_delay(Some(candidates)).await - } - - async fn get_dependencies(&self, solvable: SolvableId) -> Dependencies { - tracing::info!( - "get dependencies for {}", - self.pool - .resolve_solvable(solvable) - .name - .display(&self.pool) - ); - - let concurrent_requests = self.concurrent_requests.fetch_add(1, Ordering::SeqCst); - self.concurrent_requests_max.set( - self.concurrent_requests_max - .get() - .max(concurrent_requests + 1), - ); - - assert!( - self.requested_dependencies.borrow_mut().insert(solvable), - "duplicate get_dependencies request" - ); - - let candidate = self.pool.resolve_solvable(solvable); - let package_name = self.pool.resolve_package_name(candidate.name); - let pack = candidate.record; - - if pack.cancel_during_get_dependencies { - self.cancel_solving.set(true); - let reason = self.pool.intern_string("cancelled"); - return self.maybe_delay(Dependencies::Unknown(reason)).await; - } - - if pack.unknown_deps { - let reason = self.pool.intern_string("could not retrieve deps"); - return self.maybe_delay(Dependencies::Unknown(reason)).await; - } - - let Some(deps) = self.packages.get(package_name).and_then(|v| v.get(&pack)) else { - return self - .maybe_delay(Dependencies::Known(Default::default())) - .await; - }; - - let mut result = KnownDependencies { - requirements: Vec::with_capacity(deps.dependencies.len()), - constrains: Vec::with_capacity(deps.constrains.len()), - }; - for req in &deps.dependencies { - let mut remaining_req_specs = req.iter(); - - let first = remaining_req_specs - .next() - .expect("Dependency spec must have at least one constraint"); - - let first_name = self.pool.intern_package_name(&first.name); - let first_version_set = self - .pool - .intern_version_set(first_name, first.versions.clone()); - - let requirement = if remaining_req_specs.len() == 0 { - first_version_set.into() - } else { - let other_version_sets = remaining_req_specs.map(|spec| { - self.pool.intern_version_set( - self.pool.intern_package_name(&spec.name), - spec.versions.clone(), - ) - }); - - self.pool - .intern_version_set_union(first_version_set, other_version_sets) - .into() - }; - - result.requirements.push(requirement); - } - - for req in &deps.constrains { - let dep_name = self.pool.intern_package_name(&req.name); - let dep_spec = self.pool.intern_version_set(dep_name, req.versions.clone()); - result.constrains.push(dep_spec); - } - - self.maybe_delay(Dependencies::Known(result)).await - } - - fn should_cancel_with_value(&self) -> Option> { - if self.cancel_solving.get() { - Some(Box::new("cancelled!".to_string())) - } else { - None - } - } -} /// Create a string from a [`Transaction`] fn transaction_to_string(interner: &impl Interner, solvables: &[SolvableId]) -> String { @@ -552,7 +28,7 @@ fn transaction_to_string(interner: &impl Interner, solvables: &[SolvableId]) -> } /// Unsat so that we can view the conflict -fn solve_unsat(provider: BundleBoxProvider, specs: &[&str]) -> String { +fn solve_unsat(mut provider: BundleBoxProvider, specs: &[&str]) -> String { let requirements = provider.requirements(specs); let mut solver = Solver::new(provider); let problem = Problem::new().requirements(requirements); @@ -586,7 +62,7 @@ fn solve_snapshot(mut provider: BundleBoxProvider, specs: &[&str]) -> String { provider.sleep_before_return = true; - let requirements = provider.parse_requirements(specs); + let requirements = provider.requirements(specs); let mut solver = Solver::new(provider).with_runtime(runtime); let problem = Problem::new().requirements(requirements); match solver.solve(problem) { @@ -611,7 +87,7 @@ fn solve_snapshot(mut provider: BundleBoxProvider, specs: &[&str]) -> String { /// Test whether we can select a version, this is the most basic operation #[test] fn test_unit_propagation_1() { - let provider = BundleBoxProvider::from_packages(&[("asdf", 1, vec![])]); + let mut provider = BundleBoxProvider::from_packages(&[("asdf", 1, vec![])]); let requirements = provider.requirements(&["asdf"]); let mut solver = Solver::new(provider); let problem = Problem::new().requirements(requirements); @@ -628,7 +104,7 @@ fn test_unit_propagation_1() { /// Test if we can also select a nested version #[test] fn test_unit_propagation_nested() { - let provider = BundleBoxProvider::from_packages(&[ + let mut provider = BundleBoxProvider::from_packages(&[ ("asdf", 1u32, vec!["efgh"]), ("efgh", 4u32, vec![]), ("dummy", 6u32, vec![]), @@ -655,7 +131,7 @@ fn test_unit_propagation_nested() { /// Test if we can resolve multiple versions at once #[test] fn test_resolve_multiple() { - let provider = BundleBoxProvider::from_packages(&[ + let mut provider = BundleBoxProvider::from_packages(&[ ("asdf", 1, vec![]), ("asdf", 2, vec![]), ("efgh", 4, vec![]), @@ -699,6 +175,7 @@ fn test_resolve_with_concurrent_metadata_fetching() { /// In case of a conflict the version should not be selected with the conflict #[test] +#[traced_test] fn test_resolve_with_conflict() { let provider = BundleBoxProvider::from_packages(&[ ("asdf", 4, vec!["conflicting 1"]), @@ -716,7 +193,7 @@ fn test_resolve_with_conflict() { #[test] #[traced_test] fn test_resolve_with_nonexisting() { - let provider = BundleBoxProvider::from_packages(&[ + let mut provider = BundleBoxProvider::from_packages(&[ ("asdf", 4, vec!["b"]), ("asdf", 3, vec![]), ("b", 1, vec!["idontexist"]), @@ -738,7 +215,7 @@ fn test_resolve_with_nonexisting() { #[test] #[traced_test] fn test_resolve_with_nested_deps() { - let provider = BundleBoxProvider::from_packages(&[ + let mut provider = BundleBoxProvider::from_packages(&[ ( "apache-airflow", 3, @@ -907,7 +384,7 @@ fn test_resolve_favor_with_conflict() { #[test] fn test_resolve_cyclic() { - let provider = + let mut provider = BundleBoxProvider::from_packages(&[("a", 2, vec!["b 0..10"]), ("b", 5, vec!["a 2..4"])]); let requirements = provider.requirements(&["a 0..100"]); let mut solver = Solver::new(provider); @@ -1188,14 +665,14 @@ fn test_root_excluded() { #[test] fn test_constraints() { - let provider = BundleBoxProvider::from_packages(&[ + let mut provider = BundleBoxProvider::from_packages(&[ ("a", 1, vec!["b 0..10"]), ("b", 1, vec![]), ("b", 2, vec![]), ("c", 1, vec![]), ]); let requirements = provider.requirements(&["a 0..10"]); - let constraints = provider.requirements(&["b 1..2", "c"]); + let constraints = provider.version_sets(&["b 1..2", "c"]); let mut solver = Solver::new(provider); let problem = Problem::new() .requirements(requirements) @@ -1225,7 +702,7 @@ fn test_solve_with_additional() { provider.set_locked("locked", 2); let requirements = provider.requirements(&["a 0..10"]); - let constraints = provider.requirements(&["b 1..2", "c"]); + let constraints = provider.version_sets(&["b 1..2", "c"]); let extra_solvables = [ provider.solvable_id("b", 2), @@ -1277,7 +754,7 @@ fn test_solve_with_additional_with_constrains() { provider.add_package("l", 1.into(), &["j", "k"], &[]); let requirements = provider.requirements(&["a 0..10", "e"]); - let constraints = provider.requirements(&["b 1..2", "c", "k 2..3"]); + let constraints = provider.version_sets(&["b 1..2", "c", "k 2..3"]); let extra_solvables = [ provider.solvable_id("d", 1), @@ -1315,7 +792,7 @@ fn test_snapshot() { ("menu", 15, vec!["dropdown 2..3"]), ("menu", 10, vec!["dropdown 1..2"]), ("dropdown", 2, vec!["icons 2"]), - ("dropdown", 1, vec!["intl 3"]), + ("dropdown", 1, vec!["intl 3; if menu"]), ("icons", 2, vec![]), ("icons", 1, vec![]), ("intl", 5, vec![]), @@ -1330,15 +807,16 @@ fn test_snapshot() { serialize_snapshot(&snapshot, "snapshot_pubgrub_menu.json"); let mut snapshot_provider = snapshot.provider(); - - let menu_req = snapshot_provider.add_package_requirement(menu_name_id, "*"); + let menu_req = snapshot_provider + .add_package_requirement(menu_name_id, "*") + .into(); assert_snapshot!(solve_for_snapshot(snapshot_provider, &[menu_req], &[])); } #[test] fn test_snapshot_union_requirements() { - let provider = BundleBoxProvider::from_packages(&[ + let mut provider = BundleBoxProvider::from_packages(&[ ("icons", 2, vec![]), ("icons", 1, vec![]), ("intl", 5, vec![]), @@ -1346,21 +824,9 @@ fn test_snapshot_union_requirements() { ("union", 1, vec!["icons 2 | intl"]), ]); - let intl_name_id = provider.package_name("intl"); - let union_name_id = provider.package_name("union"); - - let snapshot = provider.into_snapshot(); - - let mut snapshot_provider = snapshot.provider(); - - let intl_req = snapshot_provider.add_package_requirement(intl_name_id, "*"); - let union_req = snapshot_provider.add_package_requirement(union_name_id, "*"); + let requirements = provider.requirements(&["intl", "union"]); - assert_snapshot!(solve_for_snapshot( - snapshot_provider, - &[intl_req, union_req], - &[] - )); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &[])); } #[test] @@ -1376,28 +842,18 @@ fn test_union_empty_requirements() { #[test] fn test_root_constraints() { - let provider = + let mut provider = BundleBoxProvider::from_packages(&[("icons", 1, vec![]), ("union", 1, vec!["icons"])]); - let union_name_id = provider.package_name("union"); - - let snapshot = provider.into_snapshot(); - - let mut snapshot_provider = snapshot.provider(); + let requirements = provider.requirements(&["union"]); + let constraints = provider.version_sets(&["union 5"]); - let union_req = snapshot_provider.add_package_requirement(union_name_id, "*"); - let union_constraint = snapshot_provider.add_package_requirement(union_name_id, "5"); - - assert_snapshot!(solve_for_snapshot( - snapshot_provider, - &[union_req], - &[union_constraint] - )); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &constraints)); } #[test] fn test_explicit_root_requirements() { - let provider = BundleBoxProvider::from_packages(&[ + let mut provider = BundleBoxProvider::from_packages(&[ // `a` depends transitively on `b` ("a", 1, vec!["b"]), // `b` depends on `c`, but the highest version of `b` constrains `c` to `<2`. @@ -1427,20 +883,173 @@ fn test_explicit_root_requirements() { "###); } +#[test] +fn test_conditional_requirements() { + let mut provider = BundleBoxProvider::from_packages(&[ + ("foo", 1, vec!["baz; if bar"]), + ("bar", 1, vec![]), + ("baz", 1, vec![]), + ]); + + let requirements = provider.requirements(&["foo", "bar"]); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &[]), @r###" + bar=1 + baz=1 + foo=1 + "###); +} + +#[test] +fn test_conditional_unsolvable() { + let mut provider = BundleBoxProvider::from_packages(&[ + ("foo", 1, vec!["baz 2; if bar"]), + ("bar", 1, vec![]), + ("baz", 1, vec![]), + ]); + + let requirements = provider.requirements(&["foo", "bar"]); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &[]), @r###" + foo * cannot be installed because there are no viable options: + └─ foo 1 would require + └─ baz >=2, <3, for which no candidates were found. + The following packages are incompatible + └─ bar * can be installed with any of the following options: + └─ bar 1 + "###); +} + +#[test] +fn test_conditional_unsolvable_without_condition() { + let mut provider = BundleBoxProvider::from_packages(&[ + ("foo", 1, vec![]), + ("foo", 2, vec!["baz 2; if bar"]), /* This will not be selected because baz 2 conflicts + * with the requirement. */ + ("bar", 1, vec![]), + ("baz", 1, vec![]), + ("baz", 2, vec![]), + ]); + + let requirements = provider.requirements(&["foo", "bar", "baz 1"]); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &[]), @r###" + bar=1 + baz=1 + foo=1 + "###); +} + +#[test] +fn test_conditional_requirements_version_set() { + let mut provider = BundleBoxProvider::from_packages(&[ + ("foo", 1, vec!["baz; if bar 1"]), + ("bar", 1, vec![]), + ("bar", 2, vec![]), + ("baz", 1, vec![]), + ]); + + let requirements = provider.requirements(&["foo", "bar"]); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &[]), @r###" + bar=2 + foo=1 + "###); +} + +#[test] +fn test_conditional_and() { + let mut provider = BundleBoxProvider::from_packages(&[ + ("foo", 1, vec!["icon; if bar and baz"]), + ("bar", 1, vec![]), + ("bar", 2, vec![]), + ("baz", 1, vec![]), + ("icon", 1, vec![]), + ]); + + let requirements = provider.requirements(&["foo", "bar", "baz"]); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &[]), @r###" + bar=2 + baz=1 + foo=1 + icon=1 + "###); +} + +#[test] +fn test_conditional_and_mismatch() { + let mut provider = BundleBoxProvider::from_packages(&[ + ("foo", 1, vec!["icon; if bar and baz"]), + ("bar", 1, vec![]), + ("baz", 1, vec![]), + ("icon", 1, vec![]), + ]); + + let requirements = provider.requirements(&["foo", "bar"]); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &[]), @r###" + bar=1 + foo=1 + "###); +} + +#[test] +fn test_conditional_or() { + let mut provider = BundleBoxProvider::from_packages(&[ + ("foo", 1, vec!["icon; if bar or baz"]), + ("bar", 1, vec![]), + ("baz", 1, vec![]), + ("icon", 1, vec![]), + ]); + + let requirements = provider.requirements(&["foo", "bar"]); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &[]), @r###" + bar=1 + foo=1 + icon=1 + "###); +} + +#[test] +fn test_conditional_complex() { + let mut provider = BundleBoxProvider::from_packages(&[ + ("foo", 1, vec!["icon; if bar and baz or menu"]), + ("bar", 1, vec![]), + ("baz", 1, vec![]), + ("icon", 1, vec![]), + ]); + + let requirements = provider.requirements(&["foo", "bar", "baz"]); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &[]), @r###" + bar=1 + baz=1 + foo=1 + icon=1 + "###); +} + +#[test] +#[traced_test] +fn test_condition_missing_requirement() { + let mut provider = + BundleBoxProvider::from_packages(&[("menu", 1, vec!["bla; if intl"]), ("intl", 1, vec![])]); + + let requirements = provider.requirements(&["menu"]); + assert_snapshot!(solve_for_snapshot(provider, &requirements, &[]), @"menu=1"); +} + #[cfg(feature = "serde")] -fn serialize_snapshot(snapshot: &DependencySnapshot, destination: impl AsRef) { +fn serialize_snapshot( + snapshot: &resolvo::snapshot::DependencySnapshot, + destination: impl AsRef, +) { let file = std::io::BufWriter::new(std::fs::File::create(destination.as_ref()).unwrap()); serde_json::to_writer_pretty(file, snapshot).unwrap() } -fn solve_for_snapshot( - provider: SnapshotProvider, - root_reqs: &[VersionSetId], +fn solve_for_snapshot( + provider: D, + root_reqs: &[ConditionalRequirement], root_constraints: &[VersionSetId], ) -> String { let mut solver = Solver::new(provider); let problem = Problem::new() - .requirements(root_reqs.iter().copied().map(Into::into).collect()) + .requirements(root_reqs.iter().cloned().collect()) .constraints(root_constraints.iter().copied().map(Into::into).collect()); match solver.solve(problem) { Ok(solvables) => transaction_to_string(solver.provider(), &solvables), diff --git a/tests/solver/snapshots/solver__condition_is_disabled.snap b/tests/solver/snapshots/solver__condition_is_disabled.snap new file mode 100644 index 00000000..ab3c1b93 --- /dev/null +++ b/tests/solver/snapshots/solver__condition_is_disabled.snap @@ -0,0 +1,8 @@ +--- +source: tests/solver.rs +expression: "solve_for_snapshot(provider, &requirements, &[])" +--- +disabled=2 +icon=1 +intl=2 +menu=2 diff --git a/tests/solver/snapshots/solver__condition_missing_requirement.snap b/tests/solver/snapshots/solver__condition_missing_requirement.snap new file mode 100644 index 00000000..2ab13fca --- /dev/null +++ b/tests/solver/snapshots/solver__condition_missing_requirement.snap @@ -0,0 +1,5 @@ +--- +source: tests/solver.rs +expression: "solve_for_snapshot(provider, &requirements, &[])" +--- +menu=1 diff --git a/tests/solver/snapshots/solver__conditional_requirements.snap b/tests/solver/snapshots/solver__conditional_requirements.snap new file mode 100644 index 00000000..25c8c915 --- /dev/null +++ b/tests/solver/snapshots/solver__conditional_requirements.snap @@ -0,0 +1,7 @@ +--- +source: tests/solver.rs +expression: "solve_for_snapshot(provider, &requirements, &[])" +--- +bar=1 +baz=1 +foo=1 diff --git a/tests/solver/snapshots/solver__conditional_requirements_version_set.snap b/tests/solver/snapshots/solver__conditional_requirements_version_set.snap new file mode 100644 index 00000000..bb1e78a0 --- /dev/null +++ b/tests/solver/snapshots/solver__conditional_requirements_version_set.snap @@ -0,0 +1,6 @@ +--- +source: tests/solver.rs +expression: "solve_for_snapshot(provider, &requirements, &[])" +--- +bar=2 +foo=1 diff --git a/tests/solver/snapshots/solver__conditional_unsolvable.snap b/tests/solver/snapshots/solver__conditional_unsolvable.snap new file mode 100644 index 00000000..ef1a50a5 --- /dev/null +++ b/tests/solver/snapshots/solver__conditional_unsolvable.snap @@ -0,0 +1,10 @@ +--- +source: tests/solver.rs +expression: "solve_for_snapshot(provider, &requirements, &[])" +--- +foo * cannot be installed because there are no viable options: +└─ foo 1 would require + └─ baz >=2, <3, for which no candidates were found. +The following packages are incompatible +└─ bar * can be installed with any of the following options: + └─ bar 1 diff --git a/tests/solver/snapshots/solver__conditional_unsolvable_without_condition.snap b/tests/solver/snapshots/solver__conditional_unsolvable_without_condition.snap new file mode 100644 index 00000000..25c8c915 --- /dev/null +++ b/tests/solver/snapshots/solver__conditional_unsolvable_without_condition.snap @@ -0,0 +1,7 @@ +--- +source: tests/solver.rs +expression: "solve_for_snapshot(provider, &requirements, &[])" +--- +bar=1 +baz=1 +foo=1 diff --git a/tests/snapshots/solver__excluded.snap b/tests/solver/snapshots/solver__excluded.snap similarity index 100% rename from tests/snapshots/solver__excluded.snap rename to tests/solver/snapshots/solver__excluded.snap diff --git a/tests/snapshots/solver__incremental_crash.snap b/tests/solver/snapshots/solver__incremental_crash.snap similarity index 100% rename from tests/snapshots/solver__incremental_crash.snap rename to tests/solver/snapshots/solver__incremental_crash.snap diff --git a/tests/snapshots/solver__merge_excluded.snap b/tests/solver/snapshots/solver__merge_excluded.snap similarity index 100% rename from tests/snapshots/solver__merge_excluded.snap rename to tests/solver/snapshots/solver__merge_excluded.snap diff --git a/tests/snapshots/solver__merge_installable.snap b/tests/solver/snapshots/solver__merge_installable.snap similarity index 100% rename from tests/snapshots/solver__merge_installable.snap rename to tests/solver/snapshots/solver__merge_installable.snap diff --git a/tests/snapshots/solver__merge_installable_non_continuous_range.snap b/tests/solver/snapshots/solver__merge_installable_non_continuous_range.snap similarity index 100% rename from tests/snapshots/solver__merge_installable_non_continuous_range.snap rename to tests/solver/snapshots/solver__merge_installable_non_continuous_range.snap diff --git a/tests/snapshots/solver__missing_dep.snap b/tests/solver/snapshots/solver__missing_dep.snap similarity index 100% rename from tests/snapshots/solver__missing_dep.snap rename to tests/solver/snapshots/solver__missing_dep.snap diff --git a/tests/snapshots/solver__no_backtracking.snap b/tests/solver/snapshots/solver__no_backtracking.snap similarity index 100% rename from tests/snapshots/solver__no_backtracking.snap rename to tests/solver/snapshots/solver__no_backtracking.snap diff --git a/tests/snapshots/solver__resolve_and_cancel.snap b/tests/solver/snapshots/solver__resolve_and_cancel.snap similarity index 100% rename from tests/snapshots/solver__resolve_and_cancel.snap rename to tests/solver/snapshots/solver__resolve_and_cancel.snap diff --git a/tests/snapshots/solver__resolve_with_concurrent_metadata_fetching.snap b/tests/solver/snapshots/solver__resolve_with_concurrent_metadata_fetching.snap similarity index 100% rename from tests/snapshots/solver__resolve_with_concurrent_metadata_fetching.snap rename to tests/solver/snapshots/solver__resolve_with_concurrent_metadata_fetching.snap diff --git a/tests/snapshots/solver__resolve_with_conflict.snap b/tests/solver/snapshots/solver__resolve_with_conflict.snap similarity index 100% rename from tests/snapshots/solver__resolve_with_conflict.snap rename to tests/solver/snapshots/solver__resolve_with_conflict.snap diff --git a/tests/solver/snapshots/solver__root_constraints.snap b/tests/solver/snapshots/solver__root_constraints.snap new file mode 100644 index 00000000..0dcdcc5e --- /dev/null +++ b/tests/solver/snapshots/solver__root_constraints.snap @@ -0,0 +1,8 @@ +--- +source: tests/solver.rs +expression: "solve_for_snapshot(provider, &requirements, &constraints)" +--- +The following packages are incompatible +└─ union * can be installed with any of the following options: + └─ union 1 +├─ the constraint union >=5, <6 cannot be fulfilled diff --git a/tests/snapshots/solver__root_excluded.snap b/tests/solver/snapshots/solver__root_excluded.snap similarity index 100% rename from tests/snapshots/solver__root_excluded.snap rename to tests/solver/snapshots/solver__root_excluded.snap diff --git a/tests/snapshots/solver__snapshot.snap b/tests/solver/snapshots/solver__snapshot.snap similarity index 100% rename from tests/snapshots/solver__snapshot.snap rename to tests/solver/snapshots/solver__snapshot.snap diff --git a/tests/snapshots/solver__snapshot_union_requirements.snap b/tests/solver/snapshots/solver__snapshot_union_requirements.snap similarity index 100% rename from tests/snapshots/solver__snapshot_union_requirements.snap rename to tests/solver/snapshots/solver__snapshot_union_requirements.snap diff --git a/tests/snapshots/solver__unsat_after_backtracking.snap b/tests/solver/snapshots/solver__unsat_after_backtracking.snap similarity index 100% rename from tests/snapshots/solver__unsat_after_backtracking.snap rename to tests/solver/snapshots/solver__unsat_after_backtracking.snap diff --git a/tests/snapshots/solver__unsat_applies_graph_compression.snap b/tests/solver/snapshots/solver__unsat_applies_graph_compression.snap similarity index 93% rename from tests/snapshots/solver__unsat_applies_graph_compression.snap rename to tests/solver/snapshots/solver__unsat_applies_graph_compression.snap index c1fb2f3e..4f43875d 100644 --- a/tests/snapshots/solver__unsat_applies_graph_compression.snap +++ b/tests/solver/snapshots/solver__unsat_applies_graph_compression.snap @@ -1,11 +1,10 @@ --- source: tests/solver.rs expression: error -snapshot_kind: text --- The following packages are incompatible ├─ c >=101, <104 can be installed with any of the following options: -│ └─ c 103 +│ └─ c 101 └─ a * cannot be installed because there are no viable options: └─ a 9 | 10 would require └─ b *, which cannot be installed because there are no viable options: diff --git a/tests/snapshots/solver__unsat_bluesky_conflict.snap b/tests/solver/snapshots/solver__unsat_bluesky_conflict.snap similarity index 100% rename from tests/snapshots/solver__unsat_bluesky_conflict.snap rename to tests/solver/snapshots/solver__unsat_bluesky_conflict.snap diff --git a/tests/snapshots/solver__unsat_constrains.snap b/tests/solver/snapshots/solver__unsat_constrains.snap similarity index 100% rename from tests/snapshots/solver__unsat_constrains.snap rename to tests/solver/snapshots/solver__unsat_constrains.snap diff --git a/tests/snapshots/solver__unsat_constrains_2.snap b/tests/solver/snapshots/solver__unsat_constrains_2.snap similarity index 100% rename from tests/snapshots/solver__unsat_constrains_2.snap rename to tests/solver/snapshots/solver__unsat_constrains_2.snap diff --git a/tests/snapshots/solver__unsat_incompatible_root_requirements.snap b/tests/solver/snapshots/solver__unsat_incompatible_root_requirements.snap similarity index 100% rename from tests/snapshots/solver__unsat_incompatible_root_requirements.snap rename to tests/solver/snapshots/solver__unsat_incompatible_root_requirements.snap diff --git a/tests/snapshots/solver__unsat_locked_and_excluded.snap b/tests/solver/snapshots/solver__unsat_locked_and_excluded.snap similarity index 100% rename from tests/snapshots/solver__unsat_locked_and_excluded.snap rename to tests/solver/snapshots/solver__unsat_locked_and_excluded.snap diff --git a/tests/snapshots/solver__unsat_missing_top_level_dep_1.snap b/tests/solver/snapshots/solver__unsat_missing_top_level_dep_1.snap similarity index 100% rename from tests/snapshots/solver__unsat_missing_top_level_dep_1.snap rename to tests/solver/snapshots/solver__unsat_missing_top_level_dep_1.snap diff --git a/tests/snapshots/solver__unsat_missing_top_level_dep_2.snap b/tests/solver/snapshots/solver__unsat_missing_top_level_dep_2.snap similarity index 100% rename from tests/snapshots/solver__unsat_missing_top_level_dep_2.snap rename to tests/solver/snapshots/solver__unsat_missing_top_level_dep_2.snap diff --git a/tests/snapshots/solver__unsat_no_candidates_for_child_1.snap b/tests/solver/snapshots/solver__unsat_no_candidates_for_child_1.snap similarity index 100% rename from tests/snapshots/solver__unsat_no_candidates_for_child_1.snap rename to tests/solver/snapshots/solver__unsat_no_candidates_for_child_1.snap diff --git a/tests/snapshots/solver__unsat_no_candidates_for_child_2.snap b/tests/solver/snapshots/solver__unsat_no_candidates_for_child_2.snap similarity index 100% rename from tests/snapshots/solver__unsat_no_candidates_for_child_2.snap rename to tests/solver/snapshots/solver__unsat_no_candidates_for_child_2.snap diff --git a/tests/snapshots/solver__unsat_pubgrub_article.snap b/tests/solver/snapshots/solver__unsat_pubgrub_article.snap similarity index 100% rename from tests/snapshots/solver__unsat_pubgrub_article.snap rename to tests/solver/snapshots/solver__unsat_pubgrub_article.snap diff --git a/tools/solve-snapshot/src/main.rs b/tools/solve-snapshot/src/main.rs index 69916f91..e6f6b3d0 100644 --- a/tools/solve-snapshot/src/main.rs +++ b/tools/solve-snapshot/src/main.rs @@ -15,7 +15,9 @@ use rand::{ prelude::IteratorRandom, rngs::StdRng, }; -use resolvo::{Problem, Requirement, Solver, UnsolvableOrCancelled, snapshot::DependencySnapshot}; +use resolvo::{ + ConditionalRequirement, Problem, Solver, UnsolvableOrCancelled, snapshot::DependencySnapshot, +}; #[derive(Parser)] #[clap(version = "0.1.0", author = "Bas Zalmstra ")] @@ -79,7 +81,7 @@ fn main() { .with_timeout(SystemTime::now().add(Duration::from_secs(opts.timeout))); // Construct a problem with a random number of requirements. - let mut requirements: Vec = Vec::new(); + let mut requirements: Vec = Vec::new(); // Determine the number of requirements to solve for. let num_requirements = rng.random_range(1..=10usize); @@ -114,7 +116,7 @@ fn main() { requirements.iter().format_with("\n", |requirement, f| { f(&format_args!( "- {}", - style(requirement.display(&provider)).dim() + style(requirement.requirement.display(&provider)).dim() )) }) ); @@ -122,7 +124,10 @@ fn main() { let problem_name = requirements .iter() .format_with("\n", |requirement, f| { - f(&format_args!("{}", requirement.display(&provider))) + f(&format_args!( + "{}", + requirement.requirement.display(&provider) + )) }) .to_string();