#![doc = document_features::document_features!()]
#![forbid(unsafe_code)]
#![allow(clippy::needless_lifetimes)]
use std::borrow::Borrow;
use std::fmt;
use std::hash::Hash;
use oxidd_core::util::{AllocResult, Borrowed};
use oxidd_core::{DiagramRules, Edge, InnerNode, LevelNo, Manager, Node, ReducedOrNew};
use oxidd_derive::Countable;
mod apply_rec;
pub struct TDDRules;
impl<E: Edge, N: InnerNode<E>> DiagramRules<E, N, TDDTerminal> for TDDRules {
type Cofactors<'a>
= N::ChildrenIter<'a>
where
N: 'a,
E: 'a;
#[inline]
fn reduce<M: Manager<Edge = E, InnerNode = N, Terminal = TDDTerminal>>(
manager: &M,
level: LevelNo,
children: impl IntoIterator<Item = E>,
) -> ReducedOrNew<E, N> {
let mut it = children.into_iter();
let t = it.next().unwrap();
let u = it.next().unwrap();
let e = it.next().unwrap();
debug_assert!(it.next().is_none());
if t == u && u == e {
manager.drop_edge(u);
manager.drop_edge(e);
ReducedOrNew::Reduced(t)
} else {
ReducedOrNew::New(N::new(level, [t, u, e]), Default::default())
}
}
#[inline]
fn cofactors(_tag: E::Tag, node: &N) -> Self::Cofactors<'_> {
node.children()
}
}
#[inline(always)]
fn reduce<M>(
manager: &M,
level: LevelNo,
t: M::Edge,
u: M::Edge,
e: M::Edge,
op: TDDOp,
) -> AllocResult<M::Edge>
where
M: Manager<Terminal = TDDTerminal>,
{
let _ = op;
let tmp = <TDDRules as DiagramRules<_, _, _>>::reduce(manager, level, [t, u, e]);
if let ReducedOrNew::Reduced(..) = &tmp {
stat!(reduced op);
}
tmp.then_insert(manager, level)
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Countable, Debug)]
#[repr(u8)]
#[allow(missing_docs)]
pub enum TDDTerminal {
False,
Unknown,
True,
}
impl std::ops::Not for TDDTerminal {
type Output = TDDTerminal;
fn not(self) -> Self::Output {
match self {
TDDTerminal::False => TDDTerminal::True,
TDDTerminal::Unknown => TDDTerminal::Unknown,
TDDTerminal::True => TDDTerminal::False,
}
}
}
impl<Tag: Default> oxidd_dump::ParseTagged<Tag> for TDDTerminal {
fn parse(s: &str) -> Option<(Self, Tag)> {
let val = match s {
"f" | "F" | "false" | "False" | "FALSE" | "⊥" | "0" => TDDTerminal::False,
"u" | "U" | "unknown" | "Unknown" | "UNKNOWN" | "?" => TDDTerminal::Unknown,
"t" | "T" | "true" | "True" | "TRUE" | "⊤" | "1" => TDDTerminal::True,
_ => return None,
};
Some((val, Tag::default()))
}
}
impl oxidd_dump::AsciiDisplay for TDDTerminal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
TDDTerminal::False => f.write_str("F"),
TDDTerminal::Unknown => f.write_str("U"),
TDDTerminal::True => f.write_str("T"),
}
}
}
impl fmt::Display for TDDTerminal {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TDDTerminal::False => f.write_str("⊥"),
TDDTerminal::Unknown => f.write_str("U"),
TDDTerminal::True => f.write_str("⊤"),
}
}
}
impl From<TDDTerminal> for Option<bool> {
fn from(value: TDDTerminal) -> Self {
match value {
TDDTerminal::False => Some(false),
TDDTerminal::Unknown => None,
TDDTerminal::True => Some(true),
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Countable, Debug)]
#[repr(u8)]
#[allow(missing_docs)]
pub enum TDDOp {
Not,
And,
Or,
Nand,
Nor,
Xor,
Equiv,
Imp,
ImpStrict,
Ite,
}
#[inline]
#[must_use]
fn collect_children<E: Edge, N: InnerNode<E>>(
node: &N,
) -> (Borrowed<'_, E>, Borrowed<'_, E>, Borrowed<'_, E>) {
debug_assert_eq!(N::ARITY, 3);
let mut it = node.children();
let t = it.next().unwrap();
let u = it.next().unwrap();
let e = it.next().unwrap();
debug_assert!(it.next().is_none());
(t, u, e)
}
enum Operation<'a, E: 'a + Edge> {
Binary(TDDOp, Borrowed<'a, E>, Borrowed<'a, E>),
Not(Borrowed<'a, E>),
Done(E),
}
#[inline]
fn terminal_bin<'a, M: Manager<Terminal = TDDTerminal>, const OP: u8>(
m: &M,
f: &'a M::Edge,
g: &'a M::Edge,
) -> Operation<'a, M::Edge> {
use Node::*;
use Operation::*;
use TDDTerminal::*;
if OP == TDDOp::And as u8 {
if f == g {
return Done(m.clone_edge(f));
}
match (m.get_node(f), m.get_node(g)) {
(Terminal(t), _) | (_, Terminal(t)) if *t.borrow() == False => {
Done(m.get_terminal(False).unwrap())
}
(Terminal(t), _) if *t.borrow() == True => Done(m.clone_edge(g)),
(_, Terminal(t)) if *t.borrow() == True => Done(m.clone_edge(f)),
_ if f > g => Binary(TDDOp::And, g.borrowed(), f.borrowed()),
_ => Binary(TDDOp::And, f.borrowed(), g.borrowed()),
}
} else if OP == TDDOp::Or as u8 {
if f == g {
return Done(m.clone_edge(f));
}
match (m.get_node(f), m.get_node(g)) {
(Terminal(t), _) | (_, Terminal(t)) if *t.borrow() == True => {
Done(m.get_terminal(True).unwrap())
}
(Terminal(t), _) if *t.borrow() == False => Done(m.clone_edge(g)),
(_, Terminal(t)) if *t.borrow() == False => Done(m.clone_edge(f)),
_ if f > g => Binary(TDDOp::Or, g.borrowed(), f.borrowed()),
_ => Binary(TDDOp::Or, f.borrowed(), g.borrowed()),
}
} else if OP == TDDOp::Nand as u8 {
if f == g {
return Not(f.borrowed());
}
match (m.get_node(f), m.get_node(g)) {
(Terminal(t), _) | (_, Terminal(t)) if *t.borrow() == False => {
Done(m.get_terminal(True).unwrap())
}
(Terminal(t), _) if *t.borrow() == True => Not(g.borrowed()),
(_, Terminal(t)) if *t.borrow() == True => Not(f.borrowed()),
_ if f > g => Binary(TDDOp::Nand, g.borrowed(), f.borrowed()),
_ => Binary(TDDOp::Nand, f.borrowed(), g.borrowed()),
}
} else if OP == TDDOp::Nor as u8 {
if f == g {
return Not(f.borrowed());
}
match (m.get_node(f), m.get_node(g)) {
(Terminal(t), _) | (_, Terminal(t)) if *t.borrow() == True => {
Done(m.get_terminal(False).unwrap())
}
(Terminal(t), _) if *t.borrow() == False => Not(g.borrowed()),
(_, Terminal(t)) if *t.borrow() == False => Not(f.borrowed()),
_ if f > g => Binary(TDDOp::Nor, g.borrowed(), f.borrowed()),
_ => Binary(TDDOp::Nor, f.borrowed(), g.borrowed()),
}
} else if OP == TDDOp::Xor as u8 {
if f == g {
return Done(m.get_terminal(False).unwrap());
}
match (m.get_node(f), m.get_node(g)) {
(Terminal(t), _) if *t.borrow() == False => Done(m.clone_edge(g)),
(_, Terminal(t)) if *t.borrow() == False => Done(m.clone_edge(f)),
(Terminal(t), _) if *t.borrow() == True => Not(g.borrowed()),
(_, Terminal(t)) if *t.borrow() == True => Not(f.borrowed()),
_ if f > g => Binary(TDDOp::Xor, g.borrowed(), f.borrowed()),
_ => Binary(TDDOp::Xor, f.borrowed(), g.borrowed()),
}
} else if OP == TDDOp::Equiv as u8 {
if f == g {
return Done(m.get_terminal(True).unwrap());
}
match (m.get_node(f), m.get_node(g)) {
(Terminal(t), _) if *t.borrow() == True => Done(m.clone_edge(g)),
(_, Terminal(t)) if *t.borrow() == True => Done(m.clone_edge(f)),
(Terminal(t), _) if *t.borrow() == False => Not(g.borrowed()),
(_, Terminal(t)) if *t.borrow() == False => Not(f.borrowed()),
_ if f > g => Binary(TDDOp::Equiv, g.borrowed(), f.borrowed()),
_ => Binary(TDDOp::Equiv, f.borrowed(), g.borrowed()),
}
} else if OP == TDDOp::Imp as u8 {
if f == g {
return Done(m.get_terminal(True).unwrap());
}
match (m.get_node(f), m.get_node(g)) {
(Terminal(t), _) if *t.borrow() == False => Done(m.get_terminal(True).unwrap()),
(_, Terminal(t)) if *t.borrow() == True => Done(m.get_terminal(True).unwrap()),
(Terminal(t), _) if *t.borrow() == True => Done(m.clone_edge(g)),
(_, Terminal(t)) if *t.borrow() == False => Not(f.borrowed()),
_ => Binary(TDDOp::Imp, f.borrowed(), g.borrowed()),
}
} else if OP == TDDOp::ImpStrict as u8 {
if f == g {
return Done(m.get_terminal(False).unwrap());
}
match (m.get_node(f), m.get_node(g)) {
(Terminal(t), _) if *t.borrow() == True => Done(m.get_terminal(False).unwrap()),
(_, Terminal(t)) if *t.borrow() == False => Done(m.get_terminal(False).unwrap()),
(Terminal(t), _) if *t.borrow() == False => Done(m.clone_edge(g)),
(_, Terminal(t)) if *t.borrow() == True => Not(f.borrowed()),
_ => Binary(TDDOp::ImpStrict, f.borrowed(), g.borrowed()),
}
} else {
unreachable!("invalid binary operator")
}
}
pub use apply_rec::TDDFunction;
#[cfg(feature = "statistics")]
struct StatCounters {
calls: std::sync::atomic::AtomicI64,
cache_queries: std::sync::atomic::AtomicI64,
cache_hits: std::sync::atomic::AtomicI64,
reduced: std::sync::atomic::AtomicI64,
}
#[cfg(feature = "statistics")]
impl StatCounters {
#[allow(clippy::declare_interior_mutable_const)]
const INIT: StatCounters = StatCounters {
calls: std::sync::atomic::AtomicI64::new(0),
cache_queries: std::sync::atomic::AtomicI64::new(0),
cache_hits: std::sync::atomic::AtomicI64::new(0),
reduced: std::sync::atomic::AtomicI64::new(0),
};
fn print(counters: &[Self]) {
for (i, ctrs) in counters.iter().enumerate() {
let calls = ctrs.calls.swap(0, std::sync::atomic::Ordering::Relaxed);
let cache_queries = ctrs
.cache_queries
.swap(0, std::sync::atomic::Ordering::Relaxed);
let cache_hits = ctrs
.cache_hits
.swap(0, std::sync::atomic::Ordering::Relaxed);
let reduced = ctrs.reduced.swap(0, std::sync::atomic::Ordering::Relaxed);
if calls == 0 {
continue;
}
let terminal_percent = (calls - cache_queries) as f32 / calls as f32 * 100.0;
let cache_hit_percent = cache_hits as f32 / cache_queries as f32 * 100.0;
let op = <TDDOp as oxidd_core::Countable>::from_usize(i);
eprintln!(" {op:?}: calls: {calls}, cache queries: {cache_queries} ({terminal_percent} % terminal cases), cache hits: {cache_hits} ({cache_hit_percent} %), reduced: {reduced}");
}
}
}
#[cfg(feature = "statistics")]
static STAT_COUNTERS: [crate::StatCounters; <TDDOp as oxidd_core::Countable>::MAX_VALUE + 1] =
[crate::StatCounters::INIT; <TDDOp as oxidd_core::Countable>::MAX_VALUE + 1];
#[cfg(feature = "statistics")]
pub fn print_stats() {
eprintln!("[oxidd_rules_tdd]");
crate::StatCounters::print(&STAT_COUNTERS);
}
macro_rules! stat {
(call $op:expr) => {
let _ = $op as usize;
#[cfg(feature = "statistics")]
STAT_COUNTERS[$op as usize]
.calls
.fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
};
(cache_query $op:expr) => {
let _ = $op as usize;
#[cfg(feature = "statistics")]
STAT_COUNTERS[$op as usize]
.cache_queries
.fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
};
(cache_hit $op:expr) => {
let _ = $op as usize;
#[cfg(feature = "statistics")]
STAT_COUNTERS[$op as usize]
.cache_hits
.fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
};
(reduced $op:expr) => {
let _ = $op as usize;
#[cfg(feature = "statistics")]
STAT_COUNTERS[$op as usize]
.reduced
.fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
};
}
pub(crate) use stat;