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}