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    /// Canonicalizes a disjunction of conditional-guard conjunctions.
59    #[must_use]
60    pub fn normalize_conditional_guard_disjunction(
61        conjunctions: impl IntoIterator<Item = impl IntoIterator<Item = ConditionalGuard>>,
62    ) -> Vec<Vec<ConditionalGuard>> {
63        let keys = conjunctions
64            .into_iter()
65            .map(|conjunction| {
66                let mut key = conjunction.into_iter().collect::<Vec<_>>();
67                key.sort();
68                key.dedup();
69                key
70            })
71            .collect();
72        minimize_disjunction_by(keys, crate::guard_algebra::guards_are_complementary)
73    }
74
75    /// Builds and minimizes a DNF from predicate conjunction alternatives.
76    #[must_use]
77    pub fn from_disjunction(
78        conjunctions: impl IntoIterator<Item = impl IntoIterator<Item = Predicate>>,
79    ) -> Self {
80        let keys = conjunctions
81            .into_iter()
82            .filter_map(normalize_conjunction)
83            .collect::<Vec<_>>();
84        let keys = minimize_disjunction_by(keys, predicates_are_complementary);
85        Self(
86            keys.into_iter()
87                .map(|key| key.into_iter().collect())
88                .collect(),
89        )
90    }
91
92    /// Reports whether the formula accepts every input.
93    #[must_use]
94    pub fn is_unconditional(&self) -> bool {
95        self.0.contains(&BTreeSet::new())
96    }
97
98    /// Reports whether the formula accepts no input.
99    #[must_use]
100    pub fn is_never(&self) -> bool {
101        self.0.is_empty()
102    }
103
104    /// Returns normalized predicate conjunctions in stable order.
105    #[must_use]
106    pub fn disjuncts(&self) -> &BTreeSet<BTreeSet<Predicate>> {
107        &self.0
108    }
109
110    /// Projects each predicate conjunction into serializable contract guards.
111    #[must_use]
112    pub fn guard_conjunctions(&self) -> Vec<Vec<Guard>> {
113        let mut seen = BTreeSet::new();
114        let mut projected = Vec::new();
115        for conjunction in &self.0 {
116            // Approximate predicates remain in the in-memory DNF consumed by
117            // schema inference, where they force abstention. The serialized
118            // inspection format cannot represent them, but it can retain the
119            // exact ambient guards that still explain where the row lives.
120            let mut guards =
121                Predicate::contract_guard_stack(&conjunction.iter().cloned().collect::<Vec<_>>());
122            Guard::canonicalize_all(&mut guards);
123            if seen.insert(guards.clone()) {
124                projected.push(guards);
125            }
126        }
127        projected
128    }
129
130    /// Returns the sole projected guard conjunction, if exactly one exists.
131    #[must_use]
132    pub fn single_guard_conjunction(&self) -> Option<Vec<Guard>> {
133        let [guards] = self.guard_conjunctions().try_into().ok()?;
134        Some(guards)
135    }
136
137    /// Returns the conjunction of this formula and `other`.
138    #[must_use]
139    pub fn conjoined(&self, other: &Self) -> Self {
140        Self::from_disjunction(self.0.iter().flat_map(|left| {
141            other.0.iter().map(|right| {
142                left.iter()
143                    .chain(right)
144                    .cloned()
145                    .collect::<Vec<Predicate>>()
146            })
147        }))
148    }
149
150    /// Conjoins this formula with one guard conjunction.
151    #[must_use]
152    pub fn conjoined_with_guards(&self, guards: impl IntoIterator<Item = Guard>) -> Self {
153        self.conjoined(&Self::from_guards(guards))
154    }
155
156    /// Union conditions after their evidence payloads are known to be equal,
157    /// re-normalizing so duplicate and subsumed disjuncts are absorbed.
158    pub fn union_absorbing(&mut self, other: Self) {
159        *self = Self::from_disjunction(std::mem::take(&mut self.0).into_iter().chain(other.0));
160    }
161
162    /// Rewrites every values path and re-normalizes the formula.
163    pub fn map_value_paths<F>(&mut self, map: &mut F)
164    where
165        F: FnMut(&str) -> String,
166    {
167        *self =
168            Self::from_disjunction(std::mem::take(&mut self.0).into_iter().map(|conjunction| {
169                conjunction
170                    .into_iter()
171                    .map(|predicate| predicate.map_value_paths(map))
172                    .collect::<Vec<_>>()
173            }));
174    }
175}
176
177impl Serialize for GuardDnf {
178    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
179    where
180        S: Serializer,
181    {
182        self.guard_conjunctions().serialize(serializer)
183    }
184}
185
186impl<'de> Deserialize<'de> for GuardDnf {
187    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
188    where
189        D: Deserializer<'de>,
190    {
191        let conjunctions = Vec::<Vec<Guard>>::deserialize(deserializer)?;
192        Ok(Self::from_guard_disjunction(conjunctions))
193    }
194}
195
196fn normalize_conjunction(
197    predicates: impl IntoIterator<Item = Predicate>,
198) -> Option<Vec<Predicate>> {
199    fn push(predicate: Predicate, normalized: &mut BTreeSet<Predicate>) -> bool {
200        match predicate {
201            Predicate::True => true,
202            Predicate::False => false,
203            Predicate::And(predicates) => predicates
204                .into_iter()
205                .all(|predicate| push(predicate, normalized)),
206            Predicate::Or(predicates) if disjunction_is_tautology(&predicates) => true,
207            predicate => {
208                if normalized
209                    .iter()
210                    .any(|other| predicates_are_contradictory(&predicate, other))
211                {
212                    return false;
213                }
214                normalized.insert(predicate);
215                true
216            }
217        }
218    }
219
220    let mut normalized = BTreeSet::new();
221    for predicate in predicates {
222        if !push(predicate, &mut normalized) {
223            return None;
224        }
225    }
226    let absorbed_disjunctions = normalized
227        .iter()
228        .filter(|predicate| match predicate {
229            Predicate::Or(alternatives) => alternatives
230                .iter()
231                .any(|alternative| normalized.contains(alternative)),
232            _ => false,
233        })
234        .cloned()
235        .collect::<Vec<_>>();
236    for predicate in absorbed_disjunctions {
237        normalized.remove(&predicate);
238    }
239    let exact = Predicate::all(
240        normalized
241            .iter()
242            .filter(|predicate| !predicate.contains_approximation())
243            .cloned()
244            .collect(),
245    );
246    normalized.retain(|predicate| {
247        let Predicate::Approximate {
248            sound_subset: Some(sound_subset),
249            ..
250        } = predicate
251        else {
252            return true;
253        };
254        !crate::predicate_bdd::exact_implies(&exact, sound_subset)
255    });
256    Some(normalized.into_iter().collect())
257}
258
259fn disjunction_is_tautology(predicates: &[Predicate]) -> bool {
260    predicates.iter().any(|predicate| {
261        predicates
262            .iter()
263            .any(|other| predicates_are_complementary(predicate, other))
264    })
265}
266
267fn predicates_are_contradictory(left: &Predicate, right: &Predicate) -> bool {
268    if predicates_are_complementary(left, right) {
269        return true;
270    }
271
272    if matches!(
273        (left, right),
274        (
275            Predicate::Guard(Guard::Eq {
276                path: left_path,
277                value: left_value,
278            }),
279            Predicate::Guard(Guard::NotEq {
280                path: right_path,
281                value: right_value,
282            })
283        ) | (
284            Predicate::Guard(Guard::NotEq {
285                path: left_path,
286                value: left_value,
287            }),
288            Predicate::Guard(Guard::Eq {
289                path: right_path,
290                value: right_value,
291            })
292        ) if left_path == right_path && left_value == right_value
293    ) {
294        return true;
295    }
296
297    match (left, right) {
298        (
299            Predicate::Guard(Guard::Eq {
300                path: left_path,
301                value: left_value,
302            }),
303            Predicate::Guard(Guard::Eq {
304                path: right_path,
305                value: right_value,
306            }),
307        ) => left_path == right_path && left_value != right_value,
308        (
309            Predicate::Guard(Guard::MatchesPattern {
310                path: pattern_path, ..
311            }),
312            Predicate::Guard(Guard::Eq {
313                path: value_path,
314                value,
315            }),
316        )
317        | (
318            Predicate::Guard(Guard::Eq {
319                path: value_path,
320                value,
321            }),
322            Predicate::Guard(Guard::MatchesPattern {
323                path: pattern_path, ..
324            }),
325        ) => pattern_path == value_path && !matches!(value, crate::GuardValue::String(_)),
326        (
327            Predicate::Guard(Guard::TypeIs {
328                path: type_path,
329                schema_type,
330            }),
331            Predicate::Guard(Guard::Eq {
332                path: value_path,
333                value,
334            }),
335        )
336        | (
337            Predicate::Guard(Guard::Eq {
338                path: value_path,
339                value,
340            }),
341            Predicate::Guard(Guard::TypeIs {
342                path: type_path,
343                schema_type,
344            }),
345        ) => type_path == value_path && !guard_value_has_schema_type(value, schema_type),
346        _ => false,
347    }
348}
349
350fn guard_value_has_schema_type(value: &crate::GuardValue, schema_type: &str) -> bool {
351    match value {
352        crate::GuardValue::String(_) => schema_type == "string",
353        crate::GuardValue::Bool(_) => schema_type == "boolean",
354        crate::GuardValue::Int(_) => matches!(schema_type, "integer" | "number"),
355        crate::GuardValue::Float(_) => schema_type == "number",
356        crate::GuardValue::Null => schema_type == "null",
357    }
358}
359
360fn predicates_are_complementary(left: &Predicate, right: &Predicate) -> bool {
361    match (left, right) {
362        (predicate, Predicate::Not(negated)) | (Predicate::Not(negated), predicate) => {
363            predicate == negated.as_ref()
364        }
365        _ => false,
366    }
367}
368
369#[cfg(test)]
370#[path = "tests/guard_dnf.rs"]
371mod tests;