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}