Skip to main content

mago_algebra/
lib.rs

1use std::hash::Hash;
2
3use foldhash::HashSet;
4use foldhash::HashSetExt;
5use indexmap::IndexMap;
6
7use mago_codex::assertion::Assertion;
8use mago_span::Span;
9use mago_word::Word;
10use mago_word::WordSet;
11
12use crate::assertion_set::AssertionSet;
13use crate::clause::Clause;
14
15pub mod assertion_set;
16pub mod clause;
17
18pub type SatisfyingAssignments = IndexMap<Word, AssertionSet>;
19pub type ActiveTruths = IndexMap<Word, HashSet<usize>>;
20
21/// Default maximum clauses during CNF saturation.
22pub const DEFAULT_SATURATION_COMPLEXITY: u16 = 8_192;
23
24/// Default maximum clauses per side in disjunction operations.
25pub const DEFAULT_DISJUNCTION_COMPLEXITY: u16 = 4_096;
26
27/// Default maximum cumulative complexity during negation.
28pub const DEFAULT_NEGATION_COMPLEXITY: u16 = 4_096;
29
30/// Default upper limit for consensus optimization.
31pub const DEFAULT_CONSENSUS_LIMIT: u16 = 256;
32
33/// Configuration thresholds for CNF formula operations.
34///
35/// These thresholds control how deeply the analyzer will explore complex logical formulas.
36/// Higher values allow more precise analysis but may significantly increase analysis time.
37/// Lower values improve speed but may reduce precision on complex conditional code.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub struct AlgebraThresholds {
40    /// Maximum number of clauses to process during saturation (default: 8192).
41    ///
42    /// Controls how many clauses the simplification algorithm will work with.
43    /// If exceeded, saturation returns an empty result to avoid performance issues.
44    pub saturation_complexity: u16,
45
46    /// Maximum number of clauses per side in disjunction operations (default: 4096).
47    ///
48    /// Controls the complexity limit for OR operations between clause sets.
49    /// If either side exceeds this, the disjunction returns an empty result.
50    pub disjunction_complexity: u16,
51
52    /// Maximum cumulative complexity during negation (default: 4096).
53    ///
54    /// Controls how complex the negation of a formula can become.
55    /// If exceeded, negation returns None to avoid exponential blowup.
56    pub negation_complexity: u16,
57
58    /// Upper limit for consensus optimization (default: 256).
59    ///
60    /// Controls when the consensus rule is applied during saturation.
61    /// Only applies when clause count is between 3 and this limit.
62    pub consensus_limit: u16,
63}
64
65impl Default for AlgebraThresholds {
66    #[inline]
67    fn default() -> Self {
68        Self {
69            saturation_complexity: DEFAULT_SATURATION_COMPLEXITY,
70            disjunction_complexity: DEFAULT_DISJUNCTION_COMPLEXITY,
71            negation_complexity: DEFAULT_NEGATION_COMPLEXITY,
72            consensus_limit: DEFAULT_CONSENSUS_LIMIT,
73        }
74    }
75}
76
77/// Reduces a set of CNF clauses by exhaustively applying logical simplification rules.
78///
79/// This function takes a set of clauses and "saturates" it by applying inference and
80/// simplification rules until a fixed point is reached. The goal is to produce a
81/// logically equivalent but simpler set of clauses.
82///
83/// ## Logic
84///
85/// The function applies several logical rules in a single pass:
86///
87/// 1. **Resolution**: Identifies clauses of the form `(P ∨ A)` and `(P ∨ ¬A)` and resolves
88///    them to a new clause `P`.
89/// 2. **Unit Propagation**: If a unit clause `(A)` exists, it simplifies any other clause
90///    containing `¬A`. For example, `(A)` and `(¬A ∨ B)` becomes `(B)`.
91/// 3. **Absorption**: Removes redundant clauses. A clause is redundant if another clause
92///    that is a strict subset of it exists. For example, `(A ∨ B)` is removed if `(A)` exists.
93/// 4. **Consensus**: Removes redundant consensus clauses. If `(A ∨ X)` and `(¬A ∨ Y)` exist,
94///    their consensus is `(X ∨ Y)`. If `(X ∨ Y)` also exists in the set, it is removed
95///    as it is logically redundant.
96///
97/// # Time Complexity
98///
99/// O(N² × K) where N = number of clauses, K = average variables per clause.
100/// In practice, N is typically small (10-50 clauses), making the quadratic loops fast.
101/// The function uses index-based tracking to avoid expensive Clause cloning.
102///
103/// # Arguments
104///
105/// * `clauses` - A slice of clauses to be simplified.
106///
107/// # Returns
108///
109/// A new `Vec<Clause>` containing the simplified, owned clauses.
110#[inline]
111pub fn saturate_clauses<'clause>(
112    clauses: impl IntoIterator<Item = &'clause Clause>,
113    thresholds: &AlgebraThresholds,
114) -> Vec<Clause> {
115    fn saturate_clauses_inner(
116        unique_clauses: Vec<&Clause>,
117        saturation_complexity: usize,
118        consensus_limit: usize,
119    ) -> Vec<Clause> {
120        let unique_clauses_len = unique_clauses.len();
121        if unique_clauses_len == 0 || unique_clauses_len > saturation_complexity {
122            // If the complexity is too high, or there are no clauses, return an empty set.
123            return vec![];
124        }
125
126        let mut removed_indices: Vec<bool> = vec![false; unique_clauses_len];
127        let mut added_clauses: Vec<Clause> = Vec::new();
128
129        // Pre-built index of every (var, possibility-hash) pair that appears
130        // anywhere in the input clauses. Unit propagation needs to know
131        // whether *any* clause contains the negation of a simple clause's
132        // literal; without this index that check is an O(N) scan repeated
133        // for every simple clause, which is the dominant O(N^2) cost on big
134        // match expressions where every arm condition contributes a unit
135        // clause that has no negation in the set.
136        let mut literal_index: HashSet<(Word, u64)> = HashSet::with_capacity(unique_clauses_len * 2);
137        for clause in &unique_clauses {
138            if !clause.reconcilable || clause.wedge {
139                continue;
140            }
141            for (var_id, possibilities) in &clause.possibilities {
142                for hash in possibilities.keys() {
143                    literal_index.insert((*var_id, *hash));
144                }
145            }
146        }
147
148        // Main simplification loop for resolution and unit propagation.
149        'outer: for (clause_a_idx, clause_a) in unique_clauses.iter().enumerate() {
150            if !clause_a.reconcilable || clause_a.wedge {
151                continue;
152            }
153
154            let is_clause_a_simple = clause_a.possibilities.len() == 1
155                && clause_a.possibilities.values().next().is_some_and(|p| p.len() == 1);
156
157            if is_clause_a_simple {
158                // This is unit propagation: (A) & (!A | B) => (B)
159                // `clause_a` is the unit clause (A).
160                let Some((&clause_var, var_possibilities)) = clause_a.possibilities.iter().next() else {
161                    continue;
162                };
163
164                let Some(only_type) = var_possibilities.values().next() else {
165                    continue;
166                };
167
168                let negated_clause_type = only_type.get_negation();
169                let negated_hash = negated_clause_type.to_hash();
170
171                // Fast path: if no clause anywhere in the set contains the
172                // negation of this literal, the inner scan can't possibly do
173                // anything. This collapses the dominant O(N^2) cost to O(N)
174                // when most simple clauses have no contradicting partner.
175                if !literal_index.contains(&(clause_var, negated_hash)) {
176                    continue;
177                }
178
179                // Simple O(N) scan - fast for typical small N
180                for (clause_b_idx, clause_b) in unique_clauses.iter().enumerate() {
181                    if clause_a_idx == clause_b_idx || removed_indices[clause_b_idx] {
182                        continue;
183                    }
184
185                    if !clause_b.reconcilable || clause_b.wedge {
186                        continue;
187                    }
188
189                    // Check if clause_b contains the negated literal
190                    let Some(matching_clause_possibilities) = clause_b.possibilities.get(&clause_var) else {
191                        continue;
192                    };
193
194                    if !matching_clause_possibilities.contains_key(&negated_hash) {
195                        continue;
196                    }
197
198                    let mut clause_var_possibilities = matching_clause_possibilities.clone();
199                    clause_var_possibilities.retain(|k, _| k != &negated_hash);
200
201                    removed_indices[clause_b_idx] = true;
202
203                    if clause_var_possibilities.is_empty() {
204                        if let Some(updated_clause) = clause_b.remove_possibilities(clause_var) {
205                            added_clauses.push(updated_clause);
206                        }
207                    } else {
208                        let updated_clause = clause_b.add_possibility(clause_var, clause_var_possibilities);
209                        added_clauses.push(updated_clause);
210                    }
211                }
212            } else {
213                // Resolution: check all other clauses with the same size
214                let clause_a_size = clause_a.possibilities.len();
215
216                'inner: for (clause_b_idx, clause_b) in unique_clauses.iter().enumerate() {
217                    if clause_a_idx >= clause_b_idx || removed_indices[clause_b_idx] {
218                        continue;
219                    }
220
221                    if !clause_b.reconcilable || clause_b.wedge {
222                        continue;
223                    }
224
225                    // Quick size check before detailed comparison
226                    if clause_b.possibilities.len() != clause_a_size {
227                        continue;
228                    }
229
230                    let mut opposing_key = None;
231                    let mut mismatch = false;
232                    for (&key, a_possibilities) in &clause_a.possibilities {
233                        if let Some(b_possibilities) = clause_b.possibilities.get(&key) {
234                            if index_keys_match(a_possibilities, b_possibilities) {
235                                continue;
236                            }
237
238                            if a_possibilities.len() == 1
239                                && b_possibilities.len() == 1
240                                && a_possibilities.values().next().is_some_and(|a| {
241                                    b_possibilities.values().next().is_some_and(|b| a.is_negation_of(b))
242                                })
243                            {
244                                if opposing_key.is_some() {
245                                    mismatch = true;
246                                    break;
247                                }
248                                opposing_key = Some(key);
249                            } else {
250                                mismatch = true;
251                                break;
252                            }
253                        } else {
254                            mismatch = true;
255                            break;
256                        }
257                    }
258
259                    if mismatch {
260                        continue 'inner;
261                    }
262
263                    if let Some(key_to_remove) = opposing_key {
264                        removed_indices[clause_a_idx] = true;
265                        let maybe_new_clause = clause_a.remove_possibilities(key_to_remove);
266
267                        if let Some(new_clause) = maybe_new_clause {
268                            added_clauses.push(new_clause);
269                        } else {
270                            // If removing the possibility makes the clause empty, it's a success,
271                            // but no new clause is added.
272                            continue 'outer;
273                        }
274                    }
275                }
276            }
277        }
278
279        // Combine original clauses (minus removed ones) with newly added clauses.
280        let mut seen_hashes: HashSet<u32> = HashSet::with_capacity(unique_clauses_len);
281        let mut combined_clauses: Vec<Clause> = Vec::with_capacity(unique_clauses_len);
282
283        for (idx, clause) in unique_clauses.iter().enumerate() {
284            if !removed_indices[idx] && seen_hashes.insert(clause.hash) {
285                combined_clauses.push((*clause).clone());
286            }
287        }
288
289        for clause in added_clauses {
290            if seen_hashes.insert(clause.hash) {
291                combined_clauses.push(clause);
292            }
293        }
294
295        // Absorption rule: remove redundant clauses. e.g., (A | B) is redundant if A exists.
296        // A clause `a` is redundant if a smaller clause `b` exists that is a subset of `a`.
297        //
298        // Fast path: when every clause has exactly one variable in its possibilities map,
299        // no clause can be a strict subset of another (the strict-subset check requires
300        // `b.size < a.size`, which is impossible when both sizes equal 1). Skipping the
301        // O(N²) outer/inner walk is what brings exhaustive `match` analysis down from
302        // cubic to linear in the arm count.
303        let all_combined_size_one =
304            combined_clauses.iter().all(|c| c.wedge || !c.reconcilable || c.possibilities.len() == 1);
305
306        let mut simplified_clauses: Vec<Clause> = Vec::with_capacity(combined_clauses.len());
307
308        if all_combined_size_one {
309            simplified_clauses.extend(combined_clauses.iter().cloned());
310        } else {
311            for clause_a in &combined_clauses {
312                if clause_a.wedge {
313                    simplified_clauses.push(clause_a.clone());
314                    continue;
315                }
316
317                let mut is_redundant = false;
318
319                // Check if any smaller clause is a subset of clause_a
320                for clause_b in &combined_clauses {
321                    if std::ptr::eq(clause_a, clause_b) {
322                        continue;
323                    }
324
325                    if !clause_b.reconcilable || clause_b.wedge {
326                        continue;
327                    }
328
329                    // Only check if clause_b is strictly smaller
330                    if clause_b.possibilities.len() >= clause_a.possibilities.len() {
331                        continue;
332                    }
333
334                    if clause_a.contains(clause_b) {
335                        is_redundant = true;
336                        break;
337                    }
338                }
339
340                if !is_redundant {
341                    simplified_clauses.push(clause_a.clone());
342                }
343            }
344        }
345
346        // Consensus rule: remove redundant consensus clauses.
347        // (A | X) & (!A | Y) implies (X | Y). If (X | Y) already exists, it is redundant.
348        //
349        // Fast path: when every clause has exactly one variable, the only way the rule
350        // can fire is on a directly contradictory pair `(A)` and `(!A)`. Such a pair
351        // would already have been resolved away during unit propagation above, so by
352        // the time we reach here in the all-size-one case there is nothing left to do.
353        let simplified_clauses_len = simplified_clauses.len();
354        let all_simplified_size_one = all_combined_size_one
355            && simplified_clauses.iter().all(|c| c.wedge || !c.reconcilable || c.possibilities.len() == 1);
356        if !all_simplified_size_one && simplified_clauses_len > 2 && simplified_clauses_len < consensus_limit {
357            let mut compared_clauses: HashSet<(u32, u32)> = HashSet::default();
358            let mut removed_hashes: HashSet<u32> = HashSet::default();
359
360            for (clause_a_idx, clause_a) in simplified_clauses.iter().enumerate() {
361                for clause_b in simplified_clauses.iter().skip(clause_a_idx + 1) {
362                    if compared_clauses.contains(&(clause_b.hash, clause_a.hash)) {
363                        continue;
364                    }
365
366                    compared_clauses.insert((clause_a.hash, clause_b.hash));
367
368                    // Find common keys between the two clauses
369                    let common_keys: Vec<_> = clause_a
370                        .possibilities
371                        .keys()
372                        .copied()
373                        .filter(|k| clause_b.possibilities.contains_key(k))
374                        .collect();
375
376                    if common_keys.is_empty() {
377                        continue;
378                    }
379
380                    let mut common_negated_keys: HashSet<Word> = HashSet::default();
381                    for common_key in common_keys {
382                        let a_possibilities = &clause_a.possibilities[&common_key];
383                        let b_possibilities = &clause_b.possibilities[&common_key];
384
385                        if a_possibilities.len() == 1
386                            && b_possibilities.len() == 1
387                            && a_possibilities
388                                .values()
389                                .next()
390                                .is_some_and(|a| b_possibilities.values().next().is_some_and(|b| a.is_negation_of(b)))
391                        {
392                            common_negated_keys.insert(common_key);
393                        }
394                    }
395
396                    if !common_negated_keys.is_empty() {
397                        let mut new_possibilities: IndexMap<Word, IndexMap<u64, Assertion>> = IndexMap::default();
398
399                        for (var_id, possibilities) in &clause_a.possibilities {
400                            if !common_negated_keys.contains(var_id) {
401                                new_possibilities
402                                    .entry(*var_id)
403                                    .or_default()
404                                    .extend(possibilities.iter().map(|(&k, v)| (k, v.clone())));
405                            }
406                        }
407
408                        for (var_id, possibilities) in &clause_b.possibilities {
409                            if !common_negated_keys.contains(var_id) {
410                                new_possibilities
411                                    .entry(*var_id)
412                                    .or_default()
413                                    .extend(possibilities.iter().map(|(&k, v)| (k, v.clone())));
414                            }
415                        }
416
417                        let conflict_clause =
418                            Clause::new(new_possibilities, clause_a.condition_span, clause_a.span, None, None, None);
419                        removed_hashes.insert(conflict_clause.hash);
420                    }
421                }
422            }
423
424            simplified_clauses.retain(|f| !removed_hashes.contains(&f.hash));
425        }
426
427        simplified_clauses
428    }
429
430    let mut seen: HashSet<u32> = HashSet::default();
431    let unique_clauses: Vec<&Clause> = clauses.into_iter().filter(|c| seen.insert(c.hash)).collect();
432
433    saturate_clauses_inner(unique_clauses, thresholds.saturation_complexity.into(), thresholds.consensus_limit.into())
434}
435
436/// Extracts simple assertions from a set of clauses and identifies which are "active".
437///
438/// This function iterates through a set of clauses to pull out two kinds of information:
439///
440/// 1.  A collection of all simple assertions made about variables.
441/// 2.  A subset of those assertions that are "active" based on a specific conditional context.
442///
443/// It also populates a given `HashSet` with all variable IDs referenced in the clauses.
444///
445/// Note: This function does not find a full satisfying assignment (a "model") in the
446/// SAT-solver sense. It only extracts explicitly stated assertions where a clause concerns
447/// a single variable (e.g., `$foo is TypeA | TypeB`).
448///
449/// # Arguments
450///
451/// * `clauses` - A slice of `Clause` objects to be analyzed.
452/// * `creating_conditional_id` - An optional `Span` representing a conditional context.
453///   An assertion is "active" if its `condition_span` matches this ID.
454/// * `conditionally_referenced_var_ids` - A mutable `HashSet` that will be populated with all
455///   variable IDs found in non-generated clauses.
456///
457/// # Returns
458///
459/// A tuple containing two maps:
460///
461/// 1. `SatisfyingAssignments`: Maps a variable ID to its list of possible
462///    truth assignments. Each inner `Vec<Assertion>` represents a disjunction (OR).
463/// 2. `ActiveTruths`: Maps a variable ID to a set of indices. These
464///    indices point to the "active" assertions in the first map's `Vec`.
465///
466/// # Panics
467///
468/// Panics if a reconcilable clause with exactly one possibility has an empty possibilities map.
469/// This should never happen with properly constructed clauses.
470#[inline]
471pub fn find_satisfying_assignments(
472    clauses: &[Clause],
473    creating_conditional_id: Option<Span>,
474    conditionally_referenced_var_ids: &mut WordSet,
475) -> (SatisfyingAssignments, ActiveTruths) {
476    let mut truths: IndexMap<Word, AssertionSet> = IndexMap::default();
477    let mut active_truths: IndexMap<Word, HashSet<usize>> = IndexMap::default();
478
479    for clause in clauses {
480        // Populate referenced variables from all non-generated clauses.
481        if !clause.generated {
482            for var_id in clause.possibilities.keys() {
483                // We only care about actual variables, not temporary expression placeholders.
484                if !var_id.as_bytes().starts_with(b"*") {
485                    conditionally_referenced_var_ids.insert(*var_id);
486                }
487            }
488        }
489
490        // A clause represents a "truth" if it's reconcilable and asserts a single fact
491        // about a single variable. This is an oversimplification for `possible_types.len() > 1` cases
492        // which represent ORs, but it's consistent with the original logic.
493        if !clause.reconcilable || clause.possibilities.len() != 1 {
494            continue;
495        }
496
497        // Extract the single variable and its possible assertions.
498        let Some((variable_id, possible_types)) = clause.possibilities.iter().next() else {
499            continue;
500        };
501
502        if variable_id.as_bytes().starts_with(b"*") {
503            continue;
504        }
505
506        let assertions = possible_types.values().cloned().collect::<Vec<_>>();
507        let truth_entry = truths.entry(*variable_id).or_default();
508        let new_truth_index = truth_entry.len();
509        truth_entry.push(assertions);
510
511        if let Some(creating_conditional_id) = creating_conditional_id
512            && creating_conditional_id == clause.condition_span
513        {
514            active_truths.entry(*variable_id).or_default().insert(new_truth_index);
515        }
516    }
517
518    (truths, active_truths)
519}
520
521/// Performs a logical OR (disjunction) on two sets of CNF clauses.
522///
523/// This function takes two formulas in Conjunctive Normal Form (CNF), `F1` and `F2`,
524/// and computes `F1 ∨ F2`. The result is a new formula also in CNF.
525///
526/// ## Logic
527///
528/// The implementation uses the distributive property of logic:
529/// `(A ∧ B) ∨ (C ∧ D) = (A ∨ C) ∧ (A ∨ D) ∧ (B ∨ C) ∧ (B ∨ D)`
530///
531/// This is achieved by computing the **Cartesian product** of the clauses from each set.
532/// Each clause from `left_clauses` is merged with each clause from `right_clauses` to
533/// form the new set of clauses.
534///
535/// During this process, any newly formed clause that is a **tautology** (e.g., contains
536/// `A ∨ ¬A`) is discarded, as it is always true and thus redundant in a CNF formula.
537///
538/// # Arguments
539///
540/// * `left_clauses` - A `Vec<Clause>` representing the first CNF formula.
541/// * `right_clauses` - A `Vec<Clause>` representing the second CNF formula.
542/// * `conditional_object_id` - The `Span` to assign to the newly created clauses.
543///
544/// # Returns
545///
546/// A `Vec<Clause>` representing the resulting CNF formula.
547#[inline]
548#[must_use]
549pub fn disjoin_clauses(
550    left_clauses: Vec<Clause>,
551    right_clauses: Vec<Clause>,
552    conditional_object_id: Span,
553    thresholds: &AlgebraThresholds,
554) -> Vec<Clause> {
555    let left_clauses_len = left_clauses.len();
556    let right_clauses_len = right_clauses.len();
557
558    if left_clauses_len == 0 {
559        // If there are no left clauses, return the right ones.
560        return right_clauses;
561    }
562
563    if right_clauses_len == 0 {
564        // If there are no right clauses, return the left ones.
565        return left_clauses;
566    }
567
568    if left_clauses_len > usize::from(thresholds.disjunction_complexity)
569        || right_clauses_len > usize::from(thresholds.disjunction_complexity)
570    {
571        // If either side is too complex, bail out early.
572        return vec![];
573    }
574
575    let mut clauses = Vec::with_capacity(left_clauses_len.saturating_mul(right_clauses_len));
576    let mut has_wedge = false;
577
578    // This is creating the cartesian product of two CNF formulas, which is correct for (F1 ∨ F2).
579    // (A ∧ B) ∨ (C ∧ D) becomes (A ∨ C) ∧ (A ∨ D) ∧ (B ∨ C) ∧ (B ∨ D).
580    for left_clause in left_clauses {
581        for right_clause in &right_clauses {
582            if left_clause.wedge && right_clause.wedge {
583                has_wedge = true;
584                continue;
585            }
586
587            if left_clause.wedge {
588                clauses.push(right_clause.clone());
589                continue;
590            }
591
592            if right_clause.wedge {
593                clauses.push(left_clause.clone());
594                continue;
595            }
596
597            let mut possibilities = left_clause.possibilities.clone();
598            for (var, possible_types) in &right_clause.possibilities {
599                match possibilities.get_mut(var) {
600                    Some(existing) => {
601                        existing.extend(possible_types.iter().map(|(&k, v)| (k, v.clone())));
602                    }
603                    None => {
604                        possibilities.insert(*var, possible_types.clone());
605                    }
606                }
607            }
608
609            // If a combined clause contains `A` and `!A`, it's a tautology (always true)
610            // and can be removed from a CNF formula.
611            let is_tautology = possibilities.values().any(|var_possibilities| {
612                if var_possibilities.len() > 1 {
613                    let vals = var_possibilities.values().collect::<Vec<_>>();
614                    // This is a naive check; a better one would check all pairs.
615                    for (i, v1) in vals.iter().enumerate() {
616                        for v2 in &vals[i + 1..] {
617                            if v1.is_negation_of(v2) {
618                                return true;
619                            }
620                        }
621                    }
622                }
623
624                false
625            });
626
627            if is_tautology {
628                continue;
629            }
630
631            clauses.push(Clause::new(
632                possibilities,
633                conditional_object_id,
634                conditional_object_id,
635                Some(false),
636                Some(left_clause.reconcilable && right_clause.reconcilable),
637                Some(true),
638            ));
639        }
640    }
641
642    if has_wedge {
643        // If there was `(A ∨ wedge) ∧ (B ∨ wedge)`, result is just `(A ∨ B)` which is handled, but what about just `wedge`?
644        // Let's assume a wedge means "an impossible path", so combining with OR is a no-op on the other side.
645        // The current logic of skipping seems reasonable.
646    }
647
648    clauses
649}
650
651/// Computes the logical negation of a formula in Conjunctive Normal Form (CNF).
652///
653/// This function takes a formula `F` and computes `¬F`. The negation of a CNF formula
654/// is transformed back into a simplified CNF representation.
655///
656/// ## Logic
657///
658/// The logical negation of a CNF formula typically involves applying De Morgan's laws,
659/// which results in a Disjunctive Normal Form (DNF). This function then converts the
660/// resulting DNF back into a simplified CNF.
661///
662/// The implementation achieves this by:
663/// 1.  Grouping the "impossibilities" from the input clauses, which is equivalent to
664///     negating each literal.
665/// 2.  Applying `saturate_clauses` to simplify the resulting set of clauses into a
666///     final, normalized CNF.
667///
668/// If the negation results in a contradiction (e.g., negating a tautology), the function
669/// returns a `wedge` clause, which represents a logically false state.
670///
671/// # Arguments
672///
673/// * `clauses` - A `Vec<Clause>` representing the CNF formula to be negated.
674///
675/// # Returns
676///
677/// A `Some(Vec<Clause>)` representing the negated and simplified CNF formula,
678/// or `None` if the negation is not possible due to complexity or other constraints.
679#[inline]
680#[must_use]
681pub fn negate_formula(mut clauses: Vec<Clause>, thresholds: &AlgebraThresholds) -> Option<Vec<Clause>> {
682    clauses.retain(|clause| clause.reconcilable);
683
684    if clauses.is_empty() {
685        return Some(vec![]);
686    }
687
688    let impossible_clauses = group_impossibilities(clauses, thresholds.negation_complexity.into())?;
689    if impossible_clauses.is_empty() {
690        return Some(vec![]);
691    }
692
693    let negated = saturate_clauses(impossible_clauses.iter().as_slice(), thresholds);
694
695    Some(negated)
696}
697
698#[inline]
699fn group_impossibilities(mut clauses: Vec<Clause>, max_complexity: usize) -> Option<Vec<Clause>> {
700    let mut seed_clauses = Vec::new();
701    let mut complexity = 1usize;
702
703    let Some(clause) = clauses.pop() else {
704        return Some(seed_clauses);
705    };
706
707    if !clause.wedge {
708        for (var, possibility_map) in &clause.possibilities {
709            for assertion in possibility_map.values() {
710                let impossible_type = assertion.get_negation();
711                let hash = impossible_type.to_hash();
712
713                let mut seed_clause_possibilities = IndexMap::new();
714                seed_clause_possibilities.insert(*var, IndexMap::from([(hash, impossible_type)]));
715
716                let seed_clause =
717                    Clause::new(seed_clause_possibilities, clause.condition_span, clause.span, None, None, None);
718
719                seed_clauses.push(seed_clause);
720            }
721        }
722    }
723
724    if clauses.is_empty() || seed_clauses.is_empty() {
725        return Some(seed_clauses);
726    }
727
728    let mut complexity_upper_bound = seed_clauses.len();
729    for clause in &clauses {
730        let possibilities_count: usize = clause.possibilities.values().map(IndexMap::len).sum();
731
732        complexity_upper_bound = complexity_upper_bound.saturating_mul(possibilities_count);
733
734        if complexity_upper_bound > max_complexity {
735            return None;
736        }
737    }
738
739    while let Some(clause) = clauses.pop() {
740        let mut new_clauses = Vec::with_capacity(seed_clauses.len() * 4);
741
742        let clause_negations: Vec<(Word, Vec<(u64, Assertion)>)> = clause
743            .possibilities
744            .iter()
745            .map(|(var, possibility_map)| {
746                let negs = possibility_map
747                    .values()
748                    .map(|a| {
749                        let neg = a.get_negation();
750                        let hash = neg.to_hash();
751                        (hash, neg)
752                    })
753                    .collect();
754
755                (*var, negs)
756            })
757            .collect();
758
759        for grouped_clause in &seed_clauses {
760            for (var, negations) in &clause_negations {
761                'next: for (impossible_hash, impossible_type) in negations {
762                    complexity += 1;
763                    if complexity > max_complexity {
764                        return None;
765                    }
766
767                    if let Some(new_insert_value) = grouped_clause.possibilities.get(var) {
768                        for (_, a) in new_insert_value {
769                            if a.is_negation_of(impossible_type) {
770                                break 'next;
771                            }
772                        }
773                    }
774
775                    let mut new_clause_possibilities = grouped_clause.possibilities.clone();
776
777                    new_clause_possibilities
778                        .entry(*var)
779                        .or_insert_with(IndexMap::new)
780                        .insert(*impossible_hash, impossible_type.clone());
781
782                    new_clauses.push(Clause::new(
783                        new_clause_possibilities,
784                        grouped_clause.condition_span,
785                        clause.span,
786                        Some(false),
787                        Some(true),
788                        Some(true),
789                    ));
790                }
791            }
792        }
793
794        seed_clauses = new_clauses;
795    }
796
797    seed_clauses.reverse();
798
799    Some(seed_clauses)
800}
801
802#[inline]
803fn index_keys_match<T, U, V>(map1: &IndexMap<T, U>, map2: &IndexMap<T, V>) -> bool
804where
805    T: Eq + Ord + Hash,
806{
807    map1.len() == map2.len() && map1.keys().all(|k| map2.contains_key(k))
808}