Skip to main content

helm_schema_core/
guard_dnf.rs

1use std::collections::BTreeSet;
2
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4
5use crate::guard_algebra::minimize_disjunction_by;
6use crate::{ConditionalGuard, Guard, Predicate};
7
8/// Disjunction of conjunctions of typed predicates.
9///
10/// Construction removes impossible conjunctions and normalizes exact
11/// complementary resolution, absorption, and deduplication.
12#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct GuardDnf(BTreeSet<BTreeSet<Predicate>>);
14
15impl Default for GuardDnf {
16    fn default() -> Self {
17        Self::unconditional()
18    }
19}
20
21impl GuardDnf {
22    /// Returns the formula that accepts every input.
23    #[must_use]
24    pub fn unconditional() -> Self {
25        Self(BTreeSet::from([BTreeSet::new()]))
26    }
27
28    /// Returns the formula that accepts no input.
29    #[must_use]
30    pub fn never() -> Self {
31        Self(BTreeSet::new())
32    }
33
34    /// Builds a normalized DNF from one predicate conjunction.
35    #[must_use]
36    pub fn from_conjunction(predicates: impl IntoIterator<Item = Predicate>) -> Self {
37        Self::from_disjunction([predicates])
38    }
39
40    /// Builds a normalized DNF from one guard conjunction.
41    #[must_use]
42    pub fn from_guards(guards: impl IntoIterator<Item = Guard>) -> Self {
43        Self::from_conjunction(guards.into_iter().map(Predicate::from))
44    }
45
46    /// Builds a normalized DNF from guard conjunction alternatives.
47    #[must_use]
48    pub fn from_guard_disjunction(
49        conjunctions: impl IntoIterator<Item = impl IntoIterator<Item = Guard>>,
50    ) -> Self {
51        Self::from_disjunction(
52            conjunctions
53                .into_iter()
54                .map(|guards| guards.into_iter().map(Predicate::from)),
55        )
56    }
57
58    /// Project typed predicate branches to the contract-expressible guard
59    /// vocabulary while retaining their disjunction as one row condition.
60    #[must_use]
61    pub fn from_contract_predicate_disjunction(
62        conjunctions: impl IntoIterator<Item = impl IntoIterator<Item = Predicate>>,
63    ) -> Self {
64        Self::from_guard_disjunction(conjunctions.into_iter().map(|conjunction| {
65            let predicates = conjunction.into_iter().collect::<Vec<_>>();
66            Predicate::contract_guard_stack(&predicates)
67        }))
68    }
69
70    /// Retain projected alternatives until their downstream evidence payloads
71    /// can be compared; early resolution can erase a nullable or strict arm.
72    #[must_use]
73    pub fn from_contract_predicate_disjunction_preserving_evidence(
74        conjunctions: impl IntoIterator<Item = impl IntoIterator<Item = Predicate>>,
75    ) -> Self {
76        let mut condition = Self::never();
77        for conjunction in conjunctions {
78            condition
79                .union_preserving_disjuncts(Self::from_contract_predicate_conjunction(conjunction));
80        }
81        condition
82    }
83
84    /// Projects one predicate conjunction into contract guards.
85    #[must_use]
86    pub fn from_contract_predicate_conjunction(
87        predicates: impl IntoIterator<Item = Predicate>,
88    ) -> Self {
89        Self::from_contract_predicate_disjunction([predicates])
90    }
91
92    /// Canonicalizes a disjunction of conditional-guard conjunctions.
93    #[must_use]
94    pub fn normalize_conditional_guard_disjunction(
95        conjunctions: impl IntoIterator<Item = impl IntoIterator<Item = ConditionalGuard>>,
96    ) -> Vec<Vec<ConditionalGuard>> {
97        let keys = conjunctions
98            .into_iter()
99            .map(|conjunction| {
100                let mut key = conjunction.into_iter().collect::<Vec<_>>();
101                key.sort();
102                key.dedup();
103                key
104            })
105            .collect();
106        minimize_disjunction_by(keys, crate::guard_algebra::guards_are_complementary)
107    }
108
109    /// Builds and minimizes a DNF from predicate conjunction alternatives.
110    #[must_use]
111    pub fn from_disjunction(
112        conjunctions: impl IntoIterator<Item = impl IntoIterator<Item = Predicate>>,
113    ) -> Self {
114        let keys = conjunctions
115            .into_iter()
116            .filter_map(normalize_conjunction)
117            .collect::<Vec<_>>();
118        let keys = minimize_disjunction_by(keys, predicates_are_complementary);
119        Self(
120            keys.into_iter()
121                .map(|key| key.into_iter().collect())
122                .collect(),
123        )
124    }
125
126    /// Reports whether the formula accepts every input.
127    #[must_use]
128    pub fn is_unconditional(&self) -> bool {
129        self.0.contains(&BTreeSet::new())
130    }
131
132    /// Reports whether the formula accepts no input.
133    #[must_use]
134    pub fn is_never(&self) -> bool {
135        self.0.is_empty()
136    }
137
138    /// Returns normalized predicate conjunctions in stable order.
139    #[must_use]
140    pub fn disjuncts(&self) -> &BTreeSet<BTreeSet<Predicate>> {
141        &self.0
142    }
143
144    /// Projects each predicate conjunction into serializable contract guards.
145    #[must_use]
146    pub fn guard_conjunctions(&self) -> Vec<Vec<Guard>> {
147        let mut seen = BTreeSet::new();
148        let mut projected = Vec::new();
149        for conjunction in &self.0 {
150            // Approximate predicates remain in the in-memory DNF consumed by
151            // schema inference, where they force abstention. The serialized
152            // inspection format cannot represent them, but it can retain the
153            // exact ambient guards that still explain where the row lives.
154            let mut guards =
155                Predicate::contract_guard_stack(&conjunction.iter().cloned().collect::<Vec<_>>());
156            Guard::canonicalize_all(&mut guards);
157            if seen.insert(guards.clone()) {
158                projected.push(guards);
159            }
160        }
161        projected
162    }
163
164    /// Returns the sole projected guard conjunction, if exactly one exists.
165    #[must_use]
166    pub fn single_guard_conjunction(&self) -> Option<Vec<Guard>> {
167        let [guards] = self.guard_conjunctions().try_into().ok()?;
168        Some(guards)
169    }
170
171    /// Returns the conjunction of this formula and `other`.
172    #[must_use]
173    pub fn conjoined(&self, other: &Self) -> Self {
174        Self::from_disjunction(self.0.iter().flat_map(|left| {
175            other.0.iter().map(|right| {
176                left.iter()
177                    .chain(right)
178                    .cloned()
179                    .collect::<Vec<Predicate>>()
180            })
181        }))
182    }
183
184    /// Conjoins this formula with one guard conjunction.
185    #[must_use]
186    pub fn conjoined_with_guards(&self, guards: impl IntoIterator<Item = Guard>) -> Self {
187        self.conjoined(&Self::from_guards(guards))
188    }
189
190    /// Adds alternatives without absorbing subsets that may carry distinct evidence.
191    pub fn union_preserving_disjuncts(&mut self, other: Self) {
192        // Rows are combined before downstream evidence payloads are known to
193        // be equal, so absorption here could discard a more precise payload.
194        self.0.extend(other.0);
195    }
196
197    /// Union conditions after their evidence payloads are known to be
198    /// equal, re-normalizing so duplicate and subsumed disjuncts are
199    /// absorbed (unlike [`Self::union_preserving_disjuncts`]).
200    pub fn union_absorbing(&mut self, other: Self) {
201        *self = Self::from_disjunction(std::mem::take(&mut self.0).into_iter().chain(other.0));
202    }
203
204    /// Rewrites every values path and re-normalizes the formula.
205    pub fn map_value_paths<F>(&mut self, map: &mut F)
206    where
207        F: FnMut(&str) -> String,
208    {
209        *self =
210            Self::from_disjunction(std::mem::take(&mut self.0).into_iter().map(|conjunction| {
211                conjunction
212                    .into_iter()
213                    .map(|predicate| predicate.map_value_paths(map))
214                    .collect::<Vec<_>>()
215            }));
216    }
217}
218
219impl Serialize for GuardDnf {
220    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
221    where
222        S: Serializer,
223    {
224        self.guard_conjunctions().serialize(serializer)
225    }
226}
227
228impl<'de> Deserialize<'de> for GuardDnf {
229    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
230    where
231        D: Deserializer<'de>,
232    {
233        let conjunctions = Vec::<Vec<Guard>>::deserialize(deserializer)?;
234        Ok(Self::from_guard_disjunction(conjunctions))
235    }
236}
237
238fn normalize_conjunction(
239    predicates: impl IntoIterator<Item = Predicate>,
240) -> Option<Vec<Predicate>> {
241    fn push(predicate: Predicate, normalized: &mut BTreeSet<Predicate>) -> bool {
242        match predicate {
243            Predicate::True => true,
244            Predicate::False => false,
245            Predicate::And(predicates) => predicates
246                .into_iter()
247                .all(|predicate| push(predicate, normalized)),
248            predicate => {
249                if normalized
250                    .iter()
251                    .any(|other| predicates_are_contradictory(&predicate, other))
252                {
253                    return false;
254                }
255                normalized.insert(predicate);
256                true
257            }
258        }
259    }
260
261    let mut normalized = BTreeSet::new();
262    for predicate in predicates {
263        if !push(predicate, &mut normalized) {
264            return None;
265        }
266    }
267    Some(normalized.into_iter().collect())
268}
269
270fn predicates_are_contradictory(left: &Predicate, right: &Predicate) -> bool {
271    if predicates_are_complementary(left, right) {
272        return true;
273    }
274
275    matches!(
276        (left, right),
277        (
278            Predicate::Guard(Guard::Eq {
279                path: left_path,
280                value: left_value,
281            }),
282            Predicate::Guard(Guard::NotEq {
283                path: right_path,
284                value: right_value,
285            })
286        ) | (
287            Predicate::Guard(Guard::NotEq {
288                path: left_path,
289                value: left_value,
290            }),
291            Predicate::Guard(Guard::Eq {
292                path: right_path,
293                value: right_value,
294            })
295        ) if left_path == right_path && left_value == right_value
296    )
297}
298
299fn predicates_are_complementary(left: &Predicate, right: &Predicate) -> bool {
300    match (left, right) {
301        (predicate, Predicate::Not(negated)) | (Predicate::Not(negated), predicate) => {
302            predicate == negated.as_ref()
303        }
304        _ => false,
305    }
306}
307
308#[cfg(test)]
309#[path = "tests/guard_dnf.rs"]
310mod tests;