Skip to main content

mago_algebra/
assertion_set.rs

1use std::hash::BuildHasher;
2
3use foldhash::HashSet;
4use foldhash::fast::FixedState;
5
6use mago_codex::assertion::Assertion;
7
8/// A type alias representing a disjunction (an "OR" clause) of items.
9///
10/// For example, `Disjunction<Assertion>` is equivalent to `(Assertion1 OR Assertion2 OR ...)`.
11pub type Disjunction<T> = Vec<T>;
12
13/// A type alias representing a conjunction (an "AND" clause) of items.
14///
15/// For example, `Conjunction<Clause>` is equivalent to `(Clause1 AND Clause2 AND ...)`.
16pub type Conjunction<T> = Vec<T>;
17
18/// Represents a logical formula in Conjunctive Normal Form (CNF).
19///
20/// Each inner `Vec<Assertion>` is a single "OR" clause (a disjunction),
21/// and the outer `Vec` represents an "AND" of all these clauses (a conjunction).
22///
23/// For example, `vec![vec![A, B], vec![C]]` corresponds to the logical
24/// formula `(A OR B) AND (C)`.
25///
26/// See: [Conjunctive Normal Form](https://en.wikipedia.org/wiki/Conjunctive_normal_form)
27pub type AssertionSet = Conjunction<Disjunction<Assertion>>;
28
29/// Applies an `OR` operation to a formula in Conjunctive Normal Form (CNF).
30///
31/// This function takes a single `Assertion` and adds it to every existing `OR`
32/// clause within the formula. For example, applying `C` to `(A) AND (B)`
33/// results in `(A OR C) AND (B OR C)`.
34///
35/// See: [Distributive property](https://en.wikipedia.org/wiki/Distributive_property)
36#[inline]
37pub fn add_or_assertion(possibilities: &mut AssertionSet, assertion: Assertion) {
38    if possibilities.is_empty() {
39        // If the formula was empty (representing `true`), the result
40        // is a single clause with the new assertion.
41        possibilities.push(vec![assertion]);
42        return;
43    }
44
45    for clause in possibilities {
46        clause.push(assertion.clone());
47    }
48}
49
50/// Applies an `AND` operation to a formula in Conjunctive Normal Form (CNF).
51///
52/// This function takes a single `Assertion` and adds it as a new, separate `AND`
53/// clause to the formula. For example, applying `C` to `(A OR B)`
54/// results in `(A OR B) AND (C)`.
55#[inline]
56pub fn add_and_assertion(possibilities: &mut AssertionSet, assertion: Assertion) {
57    // Add a new clause containing only the new assertion.
58    possibilities.push(vec![assertion]);
59}
60
61/// Applies an `AND` operation with a new `OR` clause to a CNF formula.
62///
63/// This function adds a new clause, which is itself a disjunction of the
64/// provided assertions. For example, applying `(C OR D)` to `(A OR B)`
65/// results in `(A OR B) AND (C OR D)`.
66#[inline]
67pub fn add_and_clause(assertion_set: &mut AssertionSet, or_assertions: &[Assertion]) {
68    if or_assertions.is_empty() {
69        // An empty OR clause is equivalent to `false`. ANDing with `false`
70        // makes the entire formula `false`, represented by a single empty clause.
71        *assertion_set = vec![vec![]];
72        return;
73    }
74
75    assertion_set.push(or_assertions.to_vec());
76}
77
78/// Negates a formula in Conjunctive Normal Form (CNF).
79///
80/// This function applies De Morgan's laws to the formula. The process involves:
81/// 1. Converting the CNF formula `(A OR B) AND C` to its negated DNF form: `(NOT A AND NOT B) OR (NOT C)`.
82/// 2. Converting the resulting DNF back to CNF using the distributive property.
83#[inline]
84#[must_use]
85pub fn negate_assertion_set(assertion_set: AssertionSet) -> AssertionSet {
86    // 1. Apply De Morgan's laws to get the DNF representation.
87    //    `(A OR B) AND C` becomes `(¬A AND ¬B) OR (¬C)`.
88    let dnf: AssertionSet = assertion_set
89        .into_iter()
90        .filter(|or_clause| or_clause.iter().all(Assertion::is_negatable))
91        .map(|or_clause| or_clause.into_iter().map(|a| a.get_negation()).collect::<Vec<_>>())
92        .filter(|and_clause| !and_clause.is_empty())
93        .collect();
94
95    if dnf.is_empty() {
96        // The original formula was `true` (no clauses), so its negation is `false`.
97        // A `false` CNF is represented by a single, empty OR clause.
98        return vec![vec![]];
99    }
100
101    // 2. Convert the DNF back to CNF.
102    //    Start with the first AND clause of the DNF, converted to CNF.
103    //    e.g., `(¬A AND ¬B)` becomes `(¬A) AND (¬B)`.
104    let mut result_cnf: AssertionSet = dnf[0].iter().map(|literal| vec![literal.clone()]).collect();
105
106    // Iteratively combine the rest of the DNF clauses.
107    for and_clause in dnf.iter().skip(1) {
108        let mut next_result_cnf = AssertionSet::new();
109        // This is the distributive step: (F1) OR (A AND B) => (F1 OR A) AND (F1 OR B)
110        // where F1 is the current CNF.
111        for literal in and_clause {
112            for cnf_clause in &result_cnf {
113                let mut new_clause = cnf_clause.clone();
114                new_clause.push(literal.clone());
115                next_result_cnf.push(new_clause);
116            }
117        }
118
119        result_cnf = next_result_cnf;
120    }
121
122    result_cnf
123}
124
125/// Combines two CNF formulas with a logical `AND`, ensuring no duplicate clauses.
126///
127/// This function merges two sets of clauses, using a `HashSet` to efficiently
128/// filter out any clauses from the second set that are already present in the first.
129#[inline]
130pub fn and_assertion_sets(set_a: AssertionSet, set_b: AssertionSet) -> AssertionSet {
131    if (set_a.len() == 1 && set_a[0].is_empty()) || (set_b.len() == 1 && set_b[0].is_empty()) {
132        // If either formula is `false`, the result is `false`.
133        return vec![vec![]];
134    }
135
136    let mut result: AssertionSet = set_a;
137
138    // Create a set of hashes from the first set for fast lookups.
139    let mut existing_clause_hashes: HashSet<u64> = result.iter().map(hash_disjunction).collect();
140
141    // Only add clauses from the second set if they are not already present.
142    for disjunction in set_b {
143        let disjunction_hash = hash_disjunction(&disjunction);
144        if existing_clause_hashes.insert(disjunction_hash) {
145            result.push(disjunction);
146        }
147    }
148
149    result
150}
151
152/// Calculates a stable hash for a disjunctive clause (an `Or<Assertion>`).
153fn hash_disjunction(disjunction: &Disjunction<Assertion>) -> u64 {
154    let mut assertion_hashes: Vec<_> = disjunction.iter().map(mago_codex::assertion::Assertion::to_hash).collect();
155    assertion_hashes.sort_unstable();
156
157    FixedState::default().hash_one(&assertion_hashes)
158}
159
160#[cfg(test)]
161mod tests {
162    use mago_codex::ttype::atomic::TAtomic;
163    use mago_codex::ttype::atomic::scalar::TScalar;
164
165    use super::*;
166
167    fn assert_sets_equal(a: AssertionSet, b: AssertionSet) {
168        let mut sorted_a: Vec<_> = a
169            .into_iter()
170            .map(|mut v| {
171                v.sort();
172                v
173            })
174            .collect();
175        sorted_a.sort();
176        let mut sorted_b: Vec<_> = b
177            .into_iter()
178            .map(|mut v| {
179                v.sort();
180                v
181            })
182            .collect();
183        sorted_b.sort();
184        assert_eq!(sorted_a, sorted_b);
185    }
186
187    #[test]
188    fn test_add_or_assertion() {
189        // Start with (Truthy) AND (Falsy)
190        let mut set = vec![vec![Assertion::Truthy], vec![Assertion::Falsy]];
191        // OR with IsString => (Truthy OR IsString) AND (Falsy OR IsString)
192        add_or_assertion(&mut set, Assertion::IsType(TAtomic::Scalar(TScalar::string())));
193        let expected = vec![
194            vec![Assertion::Truthy, Assertion::IsType(TAtomic::Scalar(TScalar::string()))],
195            vec![Assertion::Falsy, Assertion::IsType(TAtomic::Scalar(TScalar::string()))],
196        ];
197        assert_sets_equal(expected, set);
198    }
199
200    #[test]
201    fn test_add_or_assertion_to_empty() {
202        let mut set = vec![];
203        add_or_assertion(&mut set, Assertion::IsType(TAtomic::Scalar(TScalar::string())));
204        let expected = vec![vec![Assertion::IsType(TAtomic::Scalar(TScalar::string()))]];
205        assert_sets_equal(expected, set);
206    }
207
208    #[test]
209    fn test_add_and_assertion() {
210        let mut set = vec![vec![Assertion::Truthy, Assertion::Falsy]];
211        add_and_assertion(&mut set, Assertion::IsType(TAtomic::Scalar(TScalar::string())));
212        let expected = vec![
213            vec![Assertion::Truthy, Assertion::Falsy],
214            vec![Assertion::IsType(TAtomic::Scalar(TScalar::string()))],
215        ];
216        assert_sets_equal(expected, set);
217    }
218
219    #[test]
220    fn test_add_and_clause() {
221        let mut set = vec![vec![Assertion::Truthy]];
222        let or_clause = vec![
223            Assertion::IsType(TAtomic::Scalar(TScalar::string())),
224            Assertion::IsType(TAtomic::Scalar(TScalar::int())),
225        ];
226        add_and_clause(&mut set, &or_clause);
227        let expected = vec![
228            vec![Assertion::Truthy],
229            vec![
230                Assertion::IsType(TAtomic::Scalar(TScalar::string())),
231                Assertion::IsType(TAtomic::Scalar(TScalar::int())),
232            ],
233        ];
234        assert_sets_equal(expected, set);
235    }
236
237    #[test]
238    fn test_add_and_empty_clause() {
239        let mut set = vec![vec![Assertion::Truthy]];
240        add_and_clause(&mut set, &[]);
241        let expected = vec![vec![]];
242        assert_sets_equal(expected, set);
243    }
244
245    #[test]
246    fn test_negate_simple_or_clause() {
247        let initial_cnf = vec![vec![
248            Assertion::IsType(TAtomic::Scalar(TScalar::string())),
249            Assertion::IsType(TAtomic::Scalar(TScalar::int())),
250        ]];
251        let expected_cnf = vec![
252            vec![Assertion::IsNotType(TAtomic::Scalar(TScalar::string()))],
253            vec![Assertion::IsNotType(TAtomic::Scalar(TScalar::int()))],
254        ];
255        assert_sets_equal(expected_cnf, negate_assertion_set(initial_cnf));
256    }
257
258    #[test]
259    fn test_negate_simple_and_clause() {
260        let initial_cnf = vec![vec![Assertion::Truthy], vec![Assertion::Falsy]];
261        let expected_cnf = vec![vec![Assertion::Falsy, Assertion::Truthy]];
262        assert_sets_equal(expected_cnf, negate_assertion_set(initial_cnf));
263    }
264}