resolvo/solver/
conditions.rs

1//! Resolvo supports conditional dependencies. E.g. `foo REQUIRES bar IF baz is
2//! selected`.
3//!
4//! In SAT terms we express the requirement `A requires B` as `¬A1 v B1 .. v
5//! B99` where A1 is a candidate of package A and B1 through B99 are candidates
6//! that match the requirement B. In logical terms we say, either we do not
7//! select A1 or we select one of matching Bs.
8//!
9//! If we add a condition C to the requirement, e.g. `A requires B if C` we can
10//! modify the requirement clause to `¬A1 v ¬(C) v B1 .. v B2`. In logical terms
11//! we say, either we do not select A1 or we do not match the condition or we
12//! select one of the matching Bs.
13//!
14//! The condition `C` however expands to another set of matching candidates
15//! (e.g. `C1 v C2 v C3`). If we insert that into the formula we get,
16//!
17//!   `¬A1 v ¬(C1 v C2 v C3) v B1 .. v B2`
18//!
19//! which expands to
20//!
21//!   `¬A1 v ¬C1 ^ ¬C2 ^ ¬C3 v B1 .. v B2`
22//!
23//! This is not in CNF form (required for SAT clauses) so we cannot use this
24//! directly. To work around that problem we instead represent the condition
25//! `¬(C)` as the complement of the version set C. E.g. if C would represent
26//! `package >=1` then the complement would represent the candidates that match
27//! `package <1`, or the candidates that do NOT match C. So if we represent the
28//! complement of C as C! the final form of clause becomes:
29//!
30//!   `¬A1 v C!1 .. v C!99 v B1 .. v B2`
31//!
32//! This introduces another edge case though. What if the complement is empty?
33//! The final format would be void of `C!n` variables so it would become `¬A1 v
34//! B1 .. v B2`, e.g. A unconditionally requires B. To fix that issue we
35//! introduce an auxiliary variable that encodes if at-least-one of the package
36//! C is selected (notated as `C_selected`). For each candidate of C we add the
37//! clause
38//!
39//!   `¬Cn v C_selected`
40//!
41//! This forces `C_selected` to become true if any `Cn` is set to true. We then
42//! modify the requirement clause to be
43//!
44//!   `¬A1 v ¬C_selected v B1 .. v B2`
45//!
46//! Note that we do not encode any clauses to force `C_selected` to be false.
47//! We argue that this state is not required to function properly. If
48//! `C_selected` would be set to false it would mean that all candidates of
49//! package C are unselectable. This would disable the requirement, e.g. B
50//! shouldnt be selected for A1. But it doesnt prevent A1 from being selected.
51//!
52//! Conditions are expressed as boolean expression trees. Internally  they are
53//! converted to Disjunctive Normal Form (DNF). A boolean expression is
54//! in DNF if it is a **disjunction (OR)** of one or more **conjunctive clauses
55//! (AND)**.
56//!
57//! We add a requires clause for each disjunction in the boolean expression. So
58//! if we have the requirement `A requires B if C or D` we add requires clause
59//! for `A requires B if C` and for `A requires B if D`.
60
61use std::num::NonZero;
62
63use crate::solver::clause::Literal;
64use crate::{
65    Condition, ConditionId, Interner, LogicalOperator, VersionSetId, internal::arena::ArenaId,
66};
67
68/// An identifier that describes a group of version sets that are combined using
69/// AND logical operators.
70#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Ord, PartialOrd)]
71pub struct DisjunctionId(NonZero<u32>);
72
73impl ArenaId for DisjunctionId {
74    fn from_usize(x: usize) -> Self {
75        // Safe because we are guaranteed that the id is non-zero by adding 1.
76        DisjunctionId(unsafe { NonZero::new_unchecked((x + 1) as u32) })
77    }
78
79    fn to_usize(self) -> usize {
80        (self.0.get() - 1) as usize
81    }
82}
83
84pub struct Disjunction {
85    /// The literals associated with this particular disjunction
86    pub literals: Vec<Literal>,
87
88    /// The top-level condition to which this disjunction belongs.
89    pub _condition: ConditionId,
90}
91
92/// Converts from a boolean expression tree as described by `condition` to a
93/// boolean formula in disjunctive normal form (DNF). Each inner Vec represents
94/// a conjunction (AND group) and the outer Vec represents the disjunction (OR
95/// group).
96pub fn convert_conditions_to_dnf<I: Interner>(
97    condition: ConditionId,
98    interner: &I,
99) -> Vec<Vec<VersionSetId>> {
100    match interner.resolve_condition(condition) {
101        Condition::Requirement(version_set) => vec![vec![version_set]],
102        Condition::Binary(LogicalOperator::Or, lhs, rhs) => {
103            let mut left_dnf = convert_conditions_to_dnf(lhs, interner);
104            let mut right_dnf = convert_conditions_to_dnf(rhs, interner);
105            left_dnf.append(&mut right_dnf);
106            left_dnf
107        }
108        Condition::Binary(LogicalOperator::And, lhs, rhs) => {
109            let left_dnf = convert_conditions_to_dnf(lhs, interner);
110            let right_dnf = convert_conditions_to_dnf(rhs, interner);
111
112            // Distribute AND over OR
113            let mut result = Vec::new();
114            for l in &left_dnf {
115                for r in &right_dnf {
116                    let mut merged = l.clone();
117                    merged.extend(r.clone());
118                    result.push(merged);
119                }
120            }
121            result
122        }
123    }
124}