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}