pubgrub/term.rs
1// SPDX-License-Identifier: MPL-2.0
2
3//! A term is the fundamental unit of operation of the PubGrub algorithm.
4//! It is a positive or negative expression regarding a set of versions.
5
6use std::fmt::{self, Display};
7
8use crate::VersionSet;
9
10/// A positive or negative expression regarding a set of versions.
11///
12/// `Positive(r)` and `Negative(r.complement())` are not equivalent:
13/// * the term `Positive(r)` is satisfied if the package is selected AND the selected version is in `r`.
14/// * the term `Negative(r.complement())` is satisfied if the package is not selected OR the selected version is in `r`.
15///
16/// A `Positive` term in the partial solution requires a version to be selected, but a `Negative` term
17/// allows for a solution that does not have that package selected.
18/// Specifically, `Positive(VS::empty())` means that there was a conflict (we need to select a version for the package
19/// but can't pick any), while `Negative(VS::full())` would mean it is fine as long as we don't select the package.
20#[derive(Debug, Clone, Eq, PartialEq)]
21pub enum Term<VS: VersionSet> {
22 /// For example, `1.0.0 <= v < 2.0.0` is a positive expression
23 /// that is evaluated true if a version is selected
24 /// and comprised between version 1.0.0 and version 2.0.0.
25 Positive(VS),
26 /// The term `not (v < 3.0.0)` is a negative expression
27 /// that is evaluated true if a version >= 3.0.0 is selected
28 /// or if no version is selected at all.
29 Negative(VS),
30}
31
32/// Base methods.
33impl<VS: VersionSet> Term<VS> {
34 /// A term that is always true.
35 pub(crate) fn any() -> Self {
36 Self::Negative(VS::empty())
37 }
38
39 /// A term that is never true.
40 pub(crate) fn empty() -> Self {
41 Self::Positive(VS::empty())
42 }
43
44 /// A positive term containing exactly that version.
45 pub(crate) fn exact(version: VS::V) -> Self {
46 Self::Positive(VS::singleton(version))
47 }
48
49 /// Simply check if a term is positive.
50 pub(crate) fn is_positive(&self) -> bool {
51 match self {
52 Self::Positive(_) => true,
53 Self::Negative(_) => false,
54 }
55 }
56
57 /// Negate a term.
58 /// Evaluation of a negated term always returns
59 /// the opposite of the evaluation of the original one.
60 pub(crate) fn negate(&self) -> Self {
61 match self {
62 Self::Positive(set) => Self::Negative(set.clone()),
63 Self::Negative(set) => Self::Positive(set.clone()),
64 }
65 }
66
67 /// Evaluate a term regarding a given choice of version.
68 pub(crate) fn contains(&self, v: &VS::V) -> bool {
69 match self {
70 Self::Positive(set) => set.contains(v),
71 Self::Negative(set) => !set.contains(v),
72 }
73 }
74
75 /// Unwrap the set contained in a positive term.
76 ///
77 /// Panics if used on a negative set.
78 pub fn unwrap_positive(&self) -> &VS {
79 match self {
80 Self::Positive(set) => set,
81 Self::Negative(set) => panic!("Negative term cannot unwrap positive set: {set:?}"),
82 }
83 }
84
85 /// Unwrap the set contained in a negative term.
86 ///
87 /// Panics if used on a positive set.
88 pub(crate) fn unwrap_negative(&self) -> &VS {
89 match self {
90 Self::Negative(set) => set,
91 Self::Positive(set) => panic!("Positive term cannot unwrap negative set: {set:?}"),
92 }
93 }
94}
95
96/// Set operations with terms.
97impl<VS: VersionSet> Term<VS> {
98 /// Compute the intersection of two terms.
99 ///
100 /// The intersection is negative (unselected package is allowed)
101 /// if all terms are negative.
102 pub(crate) fn intersection(&self, other: &Self) -> Self {
103 match (self, other) {
104 (Self::Positive(r1), Self::Positive(r2)) => Self::Positive(r1.intersection(r2)),
105 (Self::Positive(p), Self::Negative(n)) | (Self::Negative(n), Self::Positive(p)) => {
106 Self::Positive(n.complement().intersection(p))
107 }
108 (Self::Negative(r1), Self::Negative(r2)) => Self::Negative(r1.union(r2)),
109 }
110 }
111
112 /// Check whether two terms are mutually exclusive.
113 ///
114 /// An optimization for the native implementation of checking whether the intersection of two sets is empty.
115 pub(crate) fn is_disjoint(&self, other: &Self) -> bool {
116 match (self, other) {
117 (Self::Positive(r1), Self::Positive(r2)) => r1.is_disjoint(r2),
118 // Unselected package is allowed in both terms, so they are never disjoint.
119 (Self::Negative(_), Self::Negative(_)) => false,
120 // If the positive term is a subset of the negative term, it lies fully in the region that the negative
121 // term excludes.
122 (Self::Positive(p), Self::Negative(n)) | (Self::Negative(n), Self::Positive(p)) => {
123 p.subset_of(n)
124 }
125 }
126 }
127
128 /// Compute the union of two terms.
129 /// If at least one term is negative, the union is also negative (unselected package is allowed).
130 pub(crate) fn union(&self, other: &Self) -> Self {
131 match (self, other) {
132 (Self::Positive(r1), Self::Positive(r2)) => Self::Positive(r1.union(r2)),
133 (Self::Positive(p), Self::Negative(n)) | (Self::Negative(n), Self::Positive(p)) => {
134 Self::Negative(p.complement().intersection(n))
135 }
136 (Self::Negative(r1), Self::Negative(r2)) => Self::Negative(r1.intersection(r2)),
137 }
138 }
139
140 /// Indicate if this term is a subset of another term.
141 /// Just like for sets, we say that t1 is a subset of t2
142 /// if and only if t1 ∩ t2 = t1.
143 pub(crate) fn subset_of(&self, other: &Self) -> bool {
144 match (self, other) {
145 (Self::Positive(r1), Self::Positive(r2)) => r1.subset_of(r2),
146 (Self::Positive(r1), Self::Negative(r2)) => r1.is_disjoint(r2),
147 // Only a negative term allows the unselected package,
148 // so it can never be a subset of a positive term.
149 (Self::Negative(_), Self::Positive(_)) => false,
150 (Self::Negative(r1), Self::Negative(r2)) => r2.subset_of(r1),
151 }
152 }
153}
154
155/// Describe a relation between a set of terms S and another term t.
156///
157/// As a shorthand, we say that a term v
158/// satisfies or contradicts a term t if {v} satisfies or contradicts it.
159pub(crate) enum Relation {
160 /// We say that a set of terms S "satisfies" a term t
161 /// if t must be true whenever every term in S is true.
162 Satisfied,
163 /// Conversely, S "contradicts" t if t must be false
164 /// whenever every term in S is true.
165 Contradicted,
166 /// If neither of these is true we say that S is "inconclusive" for t.
167 Inconclusive,
168}
169
170/// Relation between terms.
171impl<VS: VersionSet> Term<VS> {
172 /// Check if a set of terms satisfies this term.
173 ///
174 /// We say that a set of terms S "satisfies" a term t
175 /// if t must be true whenever every term in S is true.
176 ///
177 /// It turns out that this can also be expressed with set operations:
178 /// S satisfies t if and only if ⋂ S ⊆ t
179 #[cfg(test)]
180 fn satisfied_by(&self, terms_intersection: &Self) -> bool {
181 terms_intersection.subset_of(self)
182 }
183
184 /// Check if a set of terms contradicts this term.
185 ///
186 /// We say that a set of terms S "contradicts" a term t
187 /// if t must be false whenever every term in S is true.
188 ///
189 /// It turns out that this can also be expressed with set operations:
190 /// S contradicts t if and only if ⋂ S is disjoint with t
191 /// S contradicts t if and only if (⋂ S) ⋂ t = ∅
192 #[cfg(test)]
193 fn contradicted_by(&self, terms_intersection: &Self) -> bool {
194 terms_intersection.intersection(self) == Self::empty()
195 }
196
197 /// Check if a set of terms satisfies or contradicts a given term.
198 /// Otherwise the relation is inconclusive.
199 pub(crate) fn relation_with(&self, other_terms_intersection: &Self) -> Relation {
200 if other_terms_intersection.subset_of(self) {
201 Relation::Satisfied
202 } else if self.is_disjoint(other_terms_intersection) {
203 Relation::Contradicted
204 } else {
205 Relation::Inconclusive
206 }
207 }
208}
209
210impl<VS: VersionSet> AsRef<Self> for Term<VS> {
211 fn as_ref(&self) -> &Self {
212 self
213 }
214}
215
216// REPORT ######################################################################
217
218impl<VS: VersionSet + Display> Display for Term<VS> {
219 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220 match self {
221 Self::Positive(set) => write!(f, "{set}"),
222 Self::Negative(set) => write!(f, "Not ( {set} )"),
223 }
224 }
225}
226
227// TESTS #######################################################################
228
229#[cfg(test)]
230pub mod tests {
231 use super::*;
232 use proptest::prelude::*;
233 use version_ranges::Ranges;
234
235 pub fn strategy() -> impl Strategy<Value = Term<Ranges<u32>>> {
236 prop_oneof![
237 version_ranges::proptest_strategy().prop_map(Term::Negative),
238 version_ranges::proptest_strategy().prop_map(Term::Positive),
239 ]
240 }
241 proptest! {
242
243 // Testing relation --------------------------------
244
245 #[test]
246 fn relation_with(term1 in strategy(), term2 in strategy()) {
247 match term1.relation_with(&term2) {
248 Relation::Satisfied => assert!(term1.satisfied_by(&term2)),
249 Relation::Contradicted => assert!(term1.contradicted_by(&term2)),
250 Relation::Inconclusive => {
251 assert!(!term1.satisfied_by(&term2));
252 assert!(!term1.contradicted_by(&term2));
253 }
254 }
255 }
256
257 /// Ensure that we don't wrongly convert between positive and negative ranges
258 #[test]
259 fn positive_negative(term1 in strategy(), term2 in strategy()) {
260 let intersection_positive = term1.is_positive() || term2.is_positive();
261 let union_positive = term1.is_positive() && term2.is_positive();
262 assert_eq!(term1.intersection(&term2).is_positive(), intersection_positive);
263 assert_eq!(term1.union(&term2).is_positive(), union_positive);
264 }
265
266 #[test]
267 fn is_disjoint_through_intersection(r1 in strategy(), r2 in strategy()) {
268 let disjoint_def = r1.intersection(&r2) == Term::empty();
269 assert_eq!(r1.is_disjoint(&r2), disjoint_def);
270 }
271
272 #[test]
273 fn subset_of_through_intersection(r1 in strategy(), r2 in strategy()) {
274 let disjoint_def = r1.intersection(&r2) == r1;
275 assert_eq!(r1.subset_of(&r2), disjoint_def);
276 }
277
278 #[test]
279 fn union_through_intersection(r1 in strategy(), r2 in strategy()) {
280 let union_def = r1
281 .negate()
282 .intersection(&r2.negate())
283 .negate();
284 assert_eq!(r1.union(&r2), union_def);
285 }
286 }
287}