oxidd_rules_zbdd/
lib.rs

1//! Rules and other basic definitions for zero-suppressed binary decision
2//! diagrams
3//!
4//! ## Feature flags
5#![doc = document_features::document_features!()]
6#![forbid(unsafe_code)]
7// `'id` lifetimes may make the code easier to understand
8#![allow(clippy::needless_lifetimes)]
9
10use std::fmt;
11use std::hash::Hash;
12
13use oxidd_core::util::{AllocResult, Borrowed, DropWith};
14use oxidd_core::{
15    DiagramRules, Edge, HasLevel, InnerNode, LevelNo, LevelView, Manager, ManagerEventSubscriber,
16    ReducedOrNew,
17};
18use oxidd_derive::Countable;
19
20// spell-checker:ignore symm
21
22mod apply_rec;
23mod recursor;
24
25// --- Reduction Rules ---------------------------------------------------------
26
27/// [`DiagramRules`] for simple binary decision diagrams
28pub struct ZBDDRules;
29
30impl<E: Edge, N: InnerNode<E>> DiagramRules<E, N, ZBDDTerminal> for ZBDDRules {
31    type Cofactors<'a>
32        = N::ChildrenIter<'a>
33    where
34        N: 'a,
35        E: 'a;
36
37    #[inline]
38    fn reduce<M: Manager<Edge = E, InnerNode = N, Terminal = ZBDDTerminal>>(
39        manager: &M,
40        level: LevelNo,
41        children: impl IntoIterator<Item = E>,
42    ) -> ReducedOrNew<E, N> {
43        let mut it = children.into_iter();
44        let hi = it.next().unwrap();
45        let lo = it.next().unwrap();
46        debug_assert!(it.next().is_none());
47
48        if manager.get_node(&hi).is_terminal(&ZBDDTerminal::Empty) {
49            manager.drop_edge(hi);
50            return ReducedOrNew::Reduced(lo);
51        }
52        ReducedOrNew::New(N::new(level, [hi, lo]), Default::default())
53    }
54
55    #[inline]
56    fn cofactors(_tag: E::Tag, node: &N) -> Self::Cofactors<'_> {
57        node.children()
58    }
59}
60
61#[inline(always)]
62fn reduce<M>(
63    manager: &M,
64    level: LevelNo,
65    hi: M::Edge,
66    lo: M::Edge,
67    op: ZBDDOp,
68) -> AllocResult<M::Edge>
69where
70    M: Manager<Terminal = ZBDDTerminal>,
71{
72    // We do not use `DiagramRules::reduce()` here, as the iterator is
73    // apparently not fully optimized away.
74    if manager.get_node(&hi).is_terminal(&ZBDDTerminal::Empty) {
75        stat!(reduced op);
76        manager.drop_edge(hi);
77        return Ok(lo);
78    }
79    oxidd_core::LevelView::get_or_insert(
80        &mut manager.level(level),
81        M::InnerNode::new(level, [hi, lo]),
82    )
83}
84
85#[inline(always)]
86fn reduce_borrowed<M>(
87    manager: &M,
88    level: LevelNo,
89    hi: Borrowed<M::Edge>,
90    lo: M::Edge,
91    op: ZBDDOp,
92) -> AllocResult<M::Edge>
93where
94    M: Manager<Terminal = ZBDDTerminal>,
95{
96    let _ = op;
97    if manager.get_node(&hi).is_terminal(&ZBDDTerminal::Empty) {
98        stat!(reduced op);
99        return Ok(lo);
100    }
101    ReducedOrNew::New(
102        M::InnerNode::new(level, [manager.clone_edge(&hi), lo]),
103        Default::default(),
104    )
105    .then_insert(manager, level)
106}
107
108// --- Terminal Type -----------------------------------------------------------
109
110/// Terminal nodes in simple binary decision diagrams
111#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Countable, Debug)]
112#[repr(u8)]
113pub enum ZBDDTerminal {
114    /// Represents the empty set (∅)
115    Empty,
116    /// Represents the set containing exactly the empty set ({∅})
117    Base,
118}
119
120impl<Tag: Default> oxidd_dump::ParseTagged<Tag> for ZBDDTerminal {
121    fn parse(s: &str) -> Option<(Self, Tag)> {
122        let val = match s {
123            "e" | "E" | "empty" | "Empty" | "EMPTY" | "∅" | "0" => ZBDDTerminal::Empty,
124            "b" | "B" | "base" | "Base" | "BASE" | "{∅}" => ZBDDTerminal::Base,
125            _ => return None,
126        };
127        Some((val, Tag::default()))
128    }
129}
130
131impl oxidd_dump::AsciiDisplay for ZBDDTerminal {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
133        match self {
134            ZBDDTerminal::Empty => f.write_str("E"),
135            ZBDDTerminal::Base => f.write_str("B"),
136        }
137    }
138}
139
140impl fmt::Display for ZBDDTerminal {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        match self {
143            ZBDDTerminal::Empty => f.write_str("∅"),
144            ZBDDTerminal::Base => f.write_str("{∅}"),
145        }
146    }
147}
148
149// --- ZBDD Cache --------------------------------------------------------------
150
151pub struct ZBDDCache<E> {
152    tautologies: Vec<E>,
153}
154
155impl<M> ManagerEventSubscriber<M> for ZBDDCache<M::Edge>
156where
157    M: Manager<Terminal = ZBDDTerminal> + HasZBDDCache<M::Edge>,
158{
159    #[inline(always)]
160    fn init_mut(manager: &mut M) {
161        Self::post_reorder_mut(manager);
162    }
163
164    fn pre_reorder_mut(manager: &mut M) {
165        // clear the cache top down
166        let mut ts = std::mem::take(&mut manager.zbdd_cache_mut().tautologies);
167        let mut level = 0;
168        while ts.len() > 1 {
169            if !manager.try_remove_node(ts.pop().unwrap(), level) {
170                break;
171            }
172            level += 1;
173        }
174        for e in ts.into_iter().rev() {
175            manager.drop_edge(e);
176        }
177    }
178
179    fn post_reorder_mut(manager: &mut M) {
180        // Build the tautologies bottom up
181        //
182        // Storing the edge for `ZBDDTerminal::Base` as well enables us to return
183        // `&E` instead of `E` in `Self::tautology()`, so we don't need as many
184        // clone/drop operations.
185        let mut tautologies = Vec::with_capacity(1 + manager.num_levels() as usize);
186        tautologies.push(manager.get_terminal(ZBDDTerminal::Base).unwrap());
187        for mut view in manager.levels().rev() {
188            let level = view.level_no();
189            let hi = manager.clone_edge(tautologies.last().unwrap());
190            let lo = manager.clone_edge(&hi);
191            let Ok(edge) = view.get_or_insert(M::InnerNode::new(level, [hi, lo])) else {
192                eprintln!("Out of memory");
193                std::process::abort();
194            };
195            tautologies.push(edge);
196        }
197
198        manager.zbdd_cache_mut().tautologies = tautologies;
199    }
200}
201
202pub trait HasZBDDCache<E: Edge> {
203    fn zbdd_cache(&self) -> &ZBDDCache<E>;
204    fn zbdd_cache_mut(&mut self) -> &mut ZBDDCache<E>;
205}
206impl<E: Edge, T: AsRef<ZBDDCache<E>> + AsMut<ZBDDCache<E>>> HasZBDDCache<E> for T {
207    #[inline(always)]
208    fn zbdd_cache(&self) -> &ZBDDCache<E> {
209        self.as_ref()
210    }
211    #[inline(always)]
212    fn zbdd_cache_mut(&mut self) -> &mut ZBDDCache<E> {
213        self.as_mut()
214    }
215}
216
217impl<E: Edge> DropWith<E> for ZBDDCache<E> {
218    fn drop_with(self, drop_edge: impl Fn(E)) {
219        for e in self.tautologies.into_iter().rev() {
220            drop_edge(e)
221        }
222    }
223}
224
225impl<E: Edge> ZBDDCache<E> {
226    /// Create a new `ZBDDCache`
227    pub fn new() -> Self {
228        Self {
229            tautologies: Vec::new(),
230        }
231    }
232
233    /// Get the tautology for the set of variables at `level` and below
234    #[inline]
235    fn tautology(&self, level: LevelNo) -> &E {
236        // The vector contains one entry for each level including the terminals.
237        // The terminal level comes first, the top-most level last.
238        let len = self.tautologies.len() as u32;
239        debug_assert!(
240            len > 0,
241            "ZBDDCache is empty. This is an OxiDD-internal error."
242        );
243        let rev_idx = std::cmp::min(len - 1, level);
244        &self.tautologies[(len - 1 - rev_idx) as usize]
245    }
246}
247
248impl<E: Edge> Default for ZBDDCache<E> {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254// --- Operations & Apply Implementation ---------------------------------------
255
256/// Native operations of this ZBDD implementation
257#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Countable, Debug)]
258#[repr(u8)]
259#[allow(missing_docs)]
260pub enum ZBDDOp {
261    Subset0,
262    Subset1,
263    Change,
264    Union,
265    Intsec,
266    Diff,
267    SymmDiff,
268
269    /// If-then-else
270    Ite,
271
272    /// Make a new node
273    ///
274    /// Only used by [`make_node()`] for statistical purposes.
275    MkNode,
276}
277
278/// Collect the two children of a binary node
279#[inline]
280#[must_use]
281fn collect_children<E: Edge, N: InnerNode<E>>(node: &N) -> (Borrowed<'_, E>, Borrowed<'_, E>) {
282    debug_assert_eq!(N::ARITY, 2);
283    let mut it = node.children();
284    let hi = it.next().unwrap();
285    let lo = it.next().unwrap();
286    debug_assert!(it.next().is_none());
287    (hi, lo)
288}
289
290/// Get the level number of the singleton set referenced by `edge`
291///
292/// Panics if `edge` does not point to a singleton set.
293#[inline]
294#[track_caller]
295fn singleton_level<M: Manager<Terminal = ZBDDTerminal>>(manager: &M, edge: &M::Edge) -> LevelNo
296where
297    M::InnerNode: HasLevel,
298{
299    let node = manager
300        .get_node(edge)
301        .expect_inner("expected a singleton set, got a terminal");
302    debug_assert!(
303        {
304            let (hi, lo) = collect_children(node);
305            manager.get_node(&*hi).is_terminal(&ZBDDTerminal::Base)
306                && manager.get_node(&*lo).is_terminal(&ZBDDTerminal::Empty)
307        },
308        "expected a singleton set, but the children are not the respective terminals"
309    );
310    node.level()
311}
312
313/// Create a set that corresponds to a ZBDD node at the level of `var` with
314/// the given `hi` and `lo` edges
315///
316/// `var` must be a singleton set, and `var`'s level must be above `hi`'s
317/// and `lo`'s levels. If one of these conditions is violated, the result is
318/// unspecified. Ideally the method panics.
319///
320/// The set semantics of this new node is `lo ∪ {x ∪ {var} | x ∈ hi}`, the
321/// logical equivalent is `lo ∨ (var ∧ hi|ᵥₐᵣ₌₀)`.
322pub fn make_node<M>(manager: &M, var: &M::Edge, hi: M::Edge, lo: M::Edge) -> AllocResult<M::Edge>
323where
324    M: Manager<Terminal = ZBDDTerminal>,
325    M::InnerNode: HasLevel,
326{
327    let level = singleton_level(manager, var);
328    reduce(manager, level, hi, lo, ZBDDOp::MkNode)
329}
330
331/// Get the Boolean function v for the singleton set {v} (given by `singleton`)
332///
333/// Panics if `singleton` is not a singleton set
334#[deprecated = "use `BooleanFunction::var` instead"]
335pub fn var_boolean_function<M>(manager: &M, singleton: &M::Edge) -> AllocResult<M::Edge>
336where
337    M: Manager<Terminal = ZBDDTerminal> + HasZBDDCache<M::Edge>,
338    M::InnerNode: HasLevel,
339{
340    let level = singleton_level(manager, singleton);
341    let hi = manager.clone_edge(manager.zbdd_cache().tautology(level + 1));
342    let lo = manager.get_terminal(ZBDDTerminal::Empty).unwrap();
343    let mut edge = oxidd_core::LevelView::get_or_insert(
344        &mut manager.level(level),
345        InnerNode::new(level, [hi, lo]),
346    )?;
347
348    // Build the chain bottom up. We need to skip the newly created level.
349    let levels = manager.levels().rev();
350    // skip -> for level 0, we are already done
351    for mut view in levels.skip((manager.num_levels() - level) as usize) {
352        // only use `oxidd_core::LevelView` here to mitigate confusion of Rust Analyzer
353        use oxidd_core::LevelView;
354
355        let level = view.level_no();
356        let edge2 = manager.clone_edge(&edge);
357        edge = view.get_or_insert(InnerNode::new(level, [edge, edge2]))?;
358    }
359
360    Ok(edge)
361}
362
363// --- Function Interface ------------------------------------------------------
364
365#[cfg(feature = "multi-threading")]
366pub use apply_rec::mt::ZBDDFunctionMT;
367pub use apply_rec::ZBDDFunction;
368
369// --- Statistics --------------------------------------------------------------
370
371#[cfg(feature = "statistics")]
372struct StatCounters {
373    calls: std::sync::atomic::AtomicI64,
374    cache_queries: std::sync::atomic::AtomicI64,
375    cache_hits: std::sync::atomic::AtomicI64,
376    reduced: std::sync::atomic::AtomicI64,
377}
378
379#[cfg(feature = "statistics")]
380impl StatCounters {
381    #[allow(clippy::declare_interior_mutable_const)]
382    const INIT: StatCounters = StatCounters {
383        calls: std::sync::atomic::AtomicI64::new(0),
384        cache_queries: std::sync::atomic::AtomicI64::new(0),
385        cache_hits: std::sync::atomic::AtomicI64::new(0),
386        reduced: std::sync::atomic::AtomicI64::new(0),
387    };
388
389    fn print(counters: &[Self]) {
390        // spell-checker:ignore ctrs
391        for (i, ctrs) in counters.iter().enumerate() {
392            let calls = ctrs.calls.swap(0, std::sync::atomic::Ordering::Relaxed);
393            let cache_queries = ctrs
394                .cache_queries
395                .swap(0, std::sync::atomic::Ordering::Relaxed);
396            let cache_hits = ctrs
397                .cache_hits
398                .swap(0, std::sync::atomic::Ordering::Relaxed);
399            let reduced = ctrs.reduced.swap(0, std::sync::atomic::Ordering::Relaxed);
400
401            if calls == 0 {
402                continue;
403            }
404
405            let terminal_percent = (calls - cache_queries) as f32 / calls as f32 * 100.0;
406            let cache_hit_percent = cache_hits as f32 / cache_queries as f32 * 100.0;
407            let op = <ZBDDOp as oxidd_core::Countable>::from_usize(i);
408            eprintln!("  {op:?}: calls: {calls}, cache queries: {cache_queries} ({terminal_percent} % terminal cases), cache hits: {cache_hits} ({cache_hit_percent} %), reduced: {reduced}");
409        }
410    }
411}
412
413#[cfg(feature = "statistics")]
414static STAT_COUNTERS: [crate::StatCounters; <ZBDDOp as oxidd_core::Countable>::MAX_VALUE + 1] =
415    [crate::StatCounters::INIT; <ZBDDOp as oxidd_core::Countable>::MAX_VALUE + 1];
416
417#[cfg(feature = "statistics")]
418/// Print statistics to stderr
419pub fn print_stats() {
420    eprintln!("[oxidd_rules_zbdd]");
421    crate::StatCounters::print(&STAT_COUNTERS);
422}
423
424macro_rules! stat {
425    (call $op:expr) => {
426        let _ = $op as usize;
427        #[cfg(feature = "statistics")]
428        STAT_COUNTERS[$op as usize]
429            .calls
430            .fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
431    };
432    (cache_query $op:expr) => {
433        let _ = $op as usize;
434        #[cfg(feature = "statistics")]
435        STAT_COUNTERS[$op as usize]
436            .cache_queries
437            .fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
438    };
439    (cache_hit $op:expr) => {
440        let _ = $op as usize;
441        #[cfg(feature = "statistics")]
442        STAT_COUNTERS[$op as usize]
443            .cache_hits
444            .fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
445    };
446    (reduced $op:expr) => {
447        let _ = $op as usize;
448        #[cfg(feature = "statistics")]
449        STAT_COUNTERS[$op as usize]
450            .reduced
451            .fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
452    };
453}
454
455pub(crate) use stat;