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(crate::ValuesPath) -> crate::ValuesPath,
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    /// Reconstructs only the public guard projection written by `Serialize`.
188    /// Opaque predicate details are deliberately not a round-trip wire format.
189    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
190    where
191        D: Deserializer<'de>,
192    {
193        let conjunctions = Vec::<Vec<Guard>>::deserialize(deserializer)?;
194        Ok(Self::from_guard_disjunction(conjunctions))
195    }
196}
197
198fn normalize_conjunction(
199    predicates: impl IntoIterator<Item = Predicate>,
200) -> Option<Vec<Predicate>> {
201    fn push(predicate: Predicate, normalized: &mut BTreeSet<Predicate>) -> bool {
202        match predicate {
203            Predicate::True => true,
204            Predicate::False => false,
205            Predicate::And(predicates) => predicates
206                .into_iter()
207                .all(|predicate| push(predicate, normalized)),
208            Predicate::Or(predicates) if disjunction_is_tautology(&predicates) => true,
209            predicate => {
210                if normalized
211                    .iter()
212                    .any(|other| predicates_are_contradictory(&predicate, other))
213                {
214                    return false;
215                }
216                normalized.insert(predicate);
217                true
218            }
219        }
220    }
221
222    let mut normalized = BTreeSet::new();
223    for predicate in predicates {
224        if !push(predicate, &mut normalized) {
225            return None;
226        }
227    }
228    let absorbed_disjunctions = normalized
229        .iter()
230        .filter(|predicate| match predicate {
231            Predicate::Or(alternatives) => alternatives
232                .iter()
233                .any(|alternative| normalized.contains(alternative)),
234            _ => false,
235        })
236        .cloned()
237        .collect::<Vec<_>>();
238    for predicate in absorbed_disjunctions {
239        normalized.remove(&predicate);
240    }
241    let exact = Predicate::all(
242        normalized
243            .iter()
244            .filter(|predicate| !predicate.contains_approximation())
245            .cloned()
246            .collect(),
247    );
248    normalized.retain(|predicate| {
249        let Predicate::Approximate {
250            sound_subset: Some(sound_subset),
251            ..
252        } = predicate
253        else {
254            return true;
255        };
256        !crate::predicate_bdd::exact_implies(&exact, sound_subset)
257    });
258    Some(normalized.into_iter().collect())
259}
260
261fn disjunction_is_tautology(predicates: &[Predicate]) -> bool {
262    predicates.iter().any(|predicate| {
263        predicates
264            .iter()
265            .any(|other| predicates_are_complementary(predicate, other))
266    })
267}
268
269fn predicates_are_contradictory(left: &Predicate, right: &Predicate) -> bool {
270    if predicates_are_complementary(left, right) {
271        return true;
272    }
273
274    if matches!(
275        (left, right),
276        (
277            Predicate::Guard(Guard::Eq {
278                path: left_path,
279                value: left_value,
280            }),
281            Predicate::Guard(Guard::NotEq {
282                path: right_path,
283                value: right_value,
284            })
285        ) | (
286            Predicate::Guard(Guard::NotEq {
287                path: left_path,
288                value: left_value,
289            }),
290            Predicate::Guard(Guard::Eq {
291                path: right_path,
292                value: right_value,
293            })
294        ) if left_path == right_path && left_value == right_value
295    ) {
296        return true;
297    }
298
299    match (left, right) {
300        (
301            Predicate::Guard(Guard::Eq {
302                path: left_path,
303                value: left_value,
304            }),
305            Predicate::Guard(Guard::Eq {
306                path: right_path,
307                value: right_value,
308            }),
309        ) => left_path == right_path && left_value != right_value,
310        (
311            Predicate::Guard(Guard::MatchesPattern {
312                path: pattern_path, ..
313            }),
314            Predicate::Guard(Guard::Eq {
315                path: value_path,
316                value,
317            }),
318        )
319        | (
320            Predicate::Guard(Guard::Eq {
321                path: value_path,
322                value,
323            }),
324            Predicate::Guard(Guard::MatchesPattern {
325                path: pattern_path, ..
326            }),
327        ) => pattern_path == value_path && !matches!(value, crate::GuardValue::String(_)),
328        (
329            Predicate::Guard(Guard::TypeIs {
330                path: type_path,
331                schema_type,
332            }),
333            Predicate::Guard(Guard::Eq {
334                path: value_path,
335                value,
336            }),
337        )
338        | (
339            Predicate::Guard(Guard::Eq {
340                path: value_path,
341                value,
342            }),
343            Predicate::Guard(Guard::TypeIs {
344                path: type_path,
345                schema_type,
346            }),
347        ) => type_path == value_path && !guard_value_has_schema_type(value, schema_type),
348        _ => false,
349    }
350}
351
352fn guard_value_has_schema_type(value: &crate::GuardValue, schema_type: &str) -> bool {
353    match value {
354        crate::GuardValue::String(_) => schema_type == "string",
355        crate::GuardValue::Bool(_) => schema_type == "boolean",
356        crate::GuardValue::Int(_) => matches!(schema_type, "integer" | "number"),
357        crate::GuardValue::Float(_) => schema_type == "number",
358        crate::GuardValue::Null => schema_type == "null",
359    }
360}
361
362fn predicates_are_complementary(left: &Predicate, right: &Predicate) -> bool {
363    match (left, right) {
364        (predicate, Predicate::Not(negated)) | (Predicate::Not(negated), predicate) => {
365            predicate == negated.as_ref()
366        }
367        _ => false,
368    }
369}
370
371#[cfg(test)]
372#[path = "tests/guard_dnf.rs"]
373mod tests;