espresso_logic/expression/
eval.rs

1//! Evaluation and equivalence checking for boolean expressions
2
3use super::manager::{BddNode, NodeId};
4use super::BoolExpr;
5use std::collections::HashMap;
6use std::sync::Arc;
7
8impl BoolExpr {
9    /// Check if two boolean expressions are logically equivalent
10    ///
11    /// **Note (v3.1.1+):** All `BoolExpr` instances are BDDs internally (unified architecture).
12    ///
13    /// Uses a two-phase BDD-based approach:
14    /// 1. **Fast BDD equality check** - Compare the internal BDD representations directly. BDDs use
15    ///    canonical representation, so equal BDDs guarantee equivalence. Very fast (O(e) where
16    ///    e is expression size).
17    /// 2. **Exact minimisation fallback** - If BDD representations differ, uses exact Espresso
18    ///    minimisation to guarantee equality. This handles cases where equivalent functions present
19    ///    different BDDs (not supposed to happen, but better safe than sorry). Even exact
20    ///    minimisation is typically much faster than exhaustive O(2^n) evaluation.
21    ///
22    /// # Performance
23    ///
24    /// - **BDD match**: O(1) constant-time check when BDD representations are identical
25    /// - **BDD mismatch**: Espresso exact minimisation (typically still faster than O(2^n))
26    ///
27    /// The BDD check is attempted first. If BDDs differ, Espresso exact minimisation
28    /// is used to guarantee equality in cases where equivalent functions present different BDDs.
29    ///
30    /// # BDD-Only Checking
31    ///
32    /// If you're content with BDD-only equality checking (without the Espresso fallback),
33    /// you can use the `==` operator instead, which performs a constant-time O(1) comparison
34    /// of BDD representations:
35    ///
36    /// ```
37    /// use espresso_logic::BoolExpr;
38    ///
39    /// let a = BoolExpr::variable("a");
40    /// let b = BoolExpr::variable("b");
41    /// let expr1 = a.and(&b);
42    /// let expr2 = a.and(&b);
43    ///
44    /// // Constant-time BDD comparison (O(1))
45    /// assert!(expr1 == expr2);
46    /// ```
47    ///
48    /// # Performance Comparison to v3.0
49    ///
50    /// Version 3.1.1+ uses unified BDD architecture (all expressions are BDDs from construction):
51    ///
52    /// - **BDD comparison**: O(1) constant-time check (BoolExpr is already a BDD)
53    /// - **Minimisation fallback**: Espresso heuristic (exact mode, typically efficient)
54    /// - **Old approach (v3.0)**: O(2^n) exhaustive truth table evaluation (exponential)
55    ///
56    /// This makes equivalency checking **dramatically faster** for expressions with many variables:
57    /// - 10 variables: 1,024× faster
58    /// - 20 variables: 1,048,576× faster
59    /// - 30 variables: Previously impossible, now feasible
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use espresso_logic::BoolExpr;
65    ///
66    /// let a = BoolExpr::variable("a");
67    /// let b = BoolExpr::variable("b");
68    ///
69    /// // Different structures, same logic
70    /// let expr1 = a.and(&b);
71    /// let expr2 = b.and(&a); // Commutative
72    ///
73    /// assert!(expr1.equivalent_to(&expr2));
74    /// ```
75    pub fn equivalent_to(&self, other: &BoolExpr) -> bool {
76        use crate::{Cover, CoverType};
77
78        // Handle constant expressions specially
79        let self_vars = self.collect_variables();
80        let other_vars = other.collect_variables();
81
82        if self_vars.is_empty() && other_vars.is_empty() {
83            // Both are constants - just evaluate
84            return self.evaluate(&HashMap::new()) == other.evaluate(&HashMap::new());
85        }
86
87        // OPTIMIZATION: First try BDD equality check (fast)
88        // BDDs use canonical representation, so equal BDDs mean equivalent functions
89        let self_bdd = self;
90        let other_bdd = other;
91
92        if self_bdd == other_bdd {
93            // BDDs are equal - expressions are definitely equivalent
94            return true;
95        }
96
97        // BDDs differ - fall back to exact minimization for thorough verification
98        // This handles edge cases where BDD construction might differ but functions are still equivalent
99        let mut cover = Cover::new(CoverType::F);
100
101        // Add both BDDs as separate outputs
102        if cover.add_expr(self_bdd, "expr1").is_err() {
103            return false;
104        }
105        if cover.add_expr(other_bdd, "expr2").is_err() {
106            return false;
107        }
108
109        // Minimize exactly once - if this fails, assume not equivalent
110        use crate::cover::Minimizable as _;
111        cover = match cover.minimize_exact() {
112            Ok(minimized) => minimized,
113            Err(_) => return false,
114        };
115
116        // Check if all cubes have identical output patterns for both outputs
117        // After exact minimization, if the expressions are equivalent, every cube
118        // will have the same value for both outputs (both 0 or both 1)
119        for cube in cover.cubes() {
120            let outputs = cube.outputs();
121            if outputs.len() >= 2 && outputs[0] != outputs[1] {
122                return false;
123            }
124        }
125
126        true
127    }
128
129    /// Evaluate the expression with a given variable assignment
130    ///
131    /// Traverses the BDD following the variable assignments until reaching a terminal node.
132    /// Returns the boolean value of the terminal node.
133    ///
134    /// # Examples
135    ///
136    /// ```
137    /// use espresso_logic::BoolExpr;
138    /// use std::collections::HashMap;
139    /// use std::sync::Arc;
140    ///
141    /// let a = BoolExpr::variable("a");
142    /// let b = BoolExpr::variable("b");
143    /// let expr = a.and(&b);
144    ///
145    /// let mut assignment = HashMap::new();
146    /// assignment.insert(Arc::from("a"), true);
147    /// assignment.insert(Arc::from("b"), true);
148    ///
149    /// assert_eq!(expr.evaluate(&assignment), true);
150    ///
151    /// assignment.insert(Arc::from("b"), false);
152    /// assert_eq!(expr.evaluate(&assignment), false);
153    /// ```
154    pub fn evaluate(&self, assignment: &HashMap<Arc<str>, bool>) -> bool {
155        self.evaluate_node(self.root, assignment)
156    }
157
158    /// Recursively evaluate a BDD node
159    fn evaluate_node(&self, node_id: NodeId, assignment: &HashMap<Arc<str>, bool>) -> bool {
160        // Acquire lock, extract needed data, then release before recursing
161        let node_info = {
162            let mgr = self.manager.read().unwrap();
163            match mgr.get_node(node_id) {
164                Some(BddNode::Terminal(val)) => (true, *val, 0, 0, None),
165                Some(BddNode::Decision { var, low, high }) => {
166                    let var_name = mgr
167                        .var_name(*var)
168                        .expect("Invalid variable ID in BDD evaluation");
169                    (false, false, *low, *high, Some(Arc::clone(var_name)))
170                }
171                None => panic!("Invalid node ID {} in BDD evaluation", node_id),
172            }
173        }; // Lock released here
174
175        match node_info {
176            (true, val, _, _, _) => val, // Terminal node
177            (false, _, low, high, Some(var_name)) => {
178                // Decision node: follow edge based on variable value
179                let var_value = assignment.get(&var_name).copied().unwrap_or(false);
180                if var_value {
181                    self.evaluate_node(high, assignment)
182                } else {
183                    self.evaluate_node(low, assignment)
184                }
185            }
186            _ => unreachable!(),
187        }
188    }
189}