espresso_logic/expression/
bdd.rs

1//! BDD traversal, queries, and analysis operations
2//!
3//! This module contains methods for traversing the BDD structure, extracting information,
4//! and querying properties of boolean expressions.
5
6use super::manager::{BddNode, NodeId, VarId, FALSE_NODE, TRUE_NODE};
7use super::BoolExpr;
8use std::collections::{BTreeMap, BTreeSet, HashMap};
9use std::sync::Arc;
10
11impl BoolExpr {
12    /// Collect all variables used in this expression in alphabetical order
13    ///
14    /// Returns a `BTreeSet` which maintains variables in sorted order.
15    /// This ordering is used when converting to covers for minimisation.
16    pub fn collect_variables(&self) -> BTreeSet<Arc<str>> {
17        let mut var_ids = std::collections::HashSet::new();
18        self.collect_var_ids(self.root, &mut var_ids);
19
20        // Convert var IDs to names
21        let mgr = self.manager.read().unwrap();
22        var_ids
23            .into_iter()
24            .filter_map(|id| mgr.var_name(id).cloned())
25            .collect()
26    }
27
28    /// Collect all variable IDs reachable from a node
29    fn collect_var_ids(&self, node: NodeId, vars: &mut std::collections::HashSet<VarId>) {
30        // Acquire lock, extract needed data, then release before recursing.
31        // This is safe because NodeIds are stable (nodes are never removed/reordered).
32        let node_info = {
33            let inner = self.manager.read().unwrap();
34            match inner.get_node(node) {
35                Some(BddNode::Terminal(_)) => None,
36                Some(BddNode::Decision { var, low, high }) => Some((*var, *low, *high)),
37                None => {
38                    panic!("Invalid node ID {} encountered during variable collection - this indicates a bug in the BDD implementation", node);
39                }
40            }
41        }; // Lock released here
42
43        if let Some((var, low, high)) = node_info {
44            if vars.insert(var) {
45                self.collect_var_ids(low, vars);
46                self.collect_var_ids(high, vars);
47            }
48        }
49    }
50
51    /// Extract cubes (product terms) from the BDD using cached DNF
52    ///
53    /// Returns a vector of cubes, where each cube is a map from variable name to
54    /// its literal value (true for positive literal, false for negative literal).
55    ///
56    /// Each cube represents one path from the root to the TRUE terminal.
57    ///
58    /// This method uses the DNF cache to avoid expensive BDD traversal.
59    pub fn to_cubes(&self) -> Vec<BTreeMap<Arc<str>, bool>> {
60        let dnf = self.get_or_create_dnf();
61        dnf.cubes().to_vec()
62    }
63
64    /// Extract cubes directly from BDD via traversal (bypasses cache)
65    ///
66    /// This is the internal method that actually traverses the BDD structure.
67    /// Most code should use `to_cubes()` instead, which uses caching.
68    pub(super) fn extract_cubes_from_bdd(&self) -> Vec<BTreeMap<Arc<str>, bool>> {
69        let mut results = Vec::new();
70        let mut current_path = BTreeMap::new();
71        self.extract_cubes(self.root, &mut current_path, &mut results);
72        results
73    }
74
75    /// Extract cubes recursively by traversing the BDD
76    fn extract_cubes(
77        &self,
78        node: NodeId,
79        current_path: &mut BTreeMap<Arc<str>, bool>,
80        results: &mut Vec<BTreeMap<Arc<str>, bool>>,
81    ) {
82        // Acquire lock, extract needed data, then release before recursing.
83        // This is safe because NodeIds are stable (nodes are never removed/reordered).
84        let node_info = {
85            let inner = self.manager.read().unwrap();
86            match inner.get_node(node) {
87                Some(BddNode::Terminal(true)) => Some((true, None)),
88                Some(BddNode::Terminal(false)) => Some((false, None)),
89                Some(BddNode::Decision { var, low, high }) => {
90                    let var_name = inner.var_name(*var)
91                        .expect("Invalid variable ID encountered during cube extraction - this indicates a bug in the BDD implementation");
92                    Some((false, Some((Arc::clone(var_name), *low, *high))))
93                }
94                None => {
95                    panic!("Invalid node ID {} encountered during cube extraction - this indicates a bug in the BDD implementation", node);
96                }
97            }
98        }; // Lock released here
99
100        match node_info {
101            Some((true, None)) => {
102                // Reached TRUE terminal - add current path as a cube
103                results.push(current_path.clone());
104            }
105            Some((false, None)) => {
106                // Reached FALSE terminal - this path doesn't contribute
107            }
108            Some((false, Some((var_name, low, high)))) => {
109                // Traverse low edge (var = false)
110                current_path.insert(Arc::clone(&var_name), false);
111                self.extract_cubes(low, current_path, results);
112                current_path.remove(&var_name);
113
114                // Traverse high edge (var = true)
115                current_path.insert(Arc::clone(&var_name), true);
116                self.extract_cubes(high, current_path, results);
117                current_path.remove(&var_name);
118            }
119            _ => unreachable!(),
120        }
121    }
122
123    /// Check if this expression is a terminal (constant)
124    pub fn is_terminal(&self) -> bool {
125        self.root == TRUE_NODE || self.root == FALSE_NODE
126    }
127
128    /// Check if this expression represents TRUE
129    pub fn is_true(&self) -> bool {
130        self.root == TRUE_NODE
131    }
132
133    /// Check if this expression represents FALSE
134    pub fn is_false(&self) -> bool {
135        self.root == FALSE_NODE
136    }
137
138    /// Get the number of nodes in this BDD representation
139    pub fn node_count(&self) -> usize {
140        self.count_reachable_nodes(self.root, &mut HashMap::new())
141    }
142
143    /// Count reachable nodes from a given root
144    fn count_reachable_nodes(&self, node: NodeId, visited: &mut HashMap<NodeId, ()>) -> usize {
145        if visited.contains_key(&node) {
146            return 0;
147        }
148        visited.insert(node, ());
149
150        // Acquire lock, extract needed data, then release before recursing.
151        // This is safe because NodeIds are stable (nodes are never removed/reordered).
152        let (is_terminal, low, high) = {
153            let inner = self.manager.read().unwrap();
154            match inner.get_node(node) {
155                Some(BddNode::Terminal(_)) => (true, 0, 0),
156                Some(BddNode::Decision { low, high, .. }) => (false, *low, *high),
157                None => {
158                    panic!("Invalid node ID {} encountered during node counting - this indicates a bug in the BDD implementation", node);
159                }
160            }
161        }; // Lock released here
162
163        if is_terminal {
164            1
165        } else {
166            1 + self.count_reachable_nodes(low, visited) + self.count_reachable_nodes(high, visited)
167        }
168    }
169
170    /// Get the variable count (number of distinct variables)
171    pub fn var_count(&self) -> usize {
172        let mut vars = std::collections::HashSet::new();
173        self.collect_var_ids(self.root, &mut vars);
174        vars.len()
175    }
176}