1#![doc = document_features::document_features!()]
6#![forbid(unsafe_code)]
7#![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
20mod apply_rec;
23mod recursor;
24
25pub 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 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#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Countable, Debug)]
112#[repr(u8)]
113pub enum ZBDDTerminal {
114 Empty,
116 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
149pub 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 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 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 pub fn new() -> Self {
228 Self {
229 tautologies: Vec::new(),
230 }
231 }
232
233 #[inline]
235 fn tautology(&self, level: LevelNo) -> &E {
236 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#[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 Ite,
271
272 MkNode,
276}
277
278#[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#[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
313pub 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#[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 let levels = manager.levels().rev();
350 for mut view in levels.skip((manager.num_levels() - level) as usize) {
352 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#[cfg(feature = "multi-threading")]
366pub use apply_rec::mt::ZBDDFunctionMT;
367pub use apply_rec::ZBDDFunction;
368
369#[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 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")]
418pub 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;