use crate::eval::{VarMap, truth_table};
use trit_core::algebra::Algebra;
use trit_core::ast::Expr;
use trit_core::value::Ternary;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SimplifyOptions {
pub fold_constants: bool,
pub max_vars_for_constant_fold: usize,
pub expand_implies: bool,
}
impl Default for SimplifyOptions {
fn default() -> Self {
Self {
fold_constants: true,
max_vars_for_constant_fold: 8,
expand_implies: false,
}
}
}
pub fn simplify(expr: &Expr, alg: Algebra) -> Expr {
simplify_with(expr, alg, SimplifyOptions::default())
}
pub fn simplify_with(expr: &Expr, alg: Algebra, opts: SimplifyOptions) -> Expr {
let step1 = rewrite(expr, alg, opts);
if !opts.fold_constants {
return step1;
}
let var_count = step1.variables().len();
if var_count > opts.max_vars_for_constant_fold {
return step1;
}
match constant_value(&step1, alg) {
Some(v) => Expr::Const(v),
None => step1,
}
}
pub fn constant_value(expr: &Expr, alg: Algebra) -> Option<Ternary> {
if expr.variables().is_empty() {
let asg = crate::eval::Assignment::new();
return Some(crate::eval::eval(expr, &asg, alg));
}
let tt = truth_table(expr, alg).ok()?;
let first = *tt.values.first()?;
if tt.values.iter().all(|v| *v == first) {
Some(first)
} else {
None
}
}
fn rewrite(expr: &Expr, alg: Algebra, opts: SimplifyOptions) -> Expr {
match expr {
Expr::Const(_) | Expr::Var(_) => expr.clone(),
Expr::Not(a) => {
let inner = rewrite(a, alg, opts);
if let Expr::Not(x) = &inner {
return (**x).clone();
}
if let Expr::Const(v) = inner {
return Expr::Const(alg.not(v));
}
Expr::logical_not(inner)
}
Expr::And(a, b) => {
let x = rewrite(a, alg, opts);
let y = rewrite(b, alg, opts);
rewrite_and(x, y, alg)
}
Expr::Or(a, b) => {
let x = rewrite(a, alg, opts);
let y = rewrite(b, alg, opts);
rewrite_or(x, y, alg)
}
Expr::Xor(a, b) => {
let x = rewrite(a, alg, opts);
let y = rewrite(b, alg, opts);
rewrite_xor(x, y, alg)
}
Expr::Implies(a, b) => {
let x = rewrite(a, alg, opts);
let y = rewrite(b, alg, opts);
if opts.expand_implies && alg == Algebra::Kleene {
return rewrite_or(Expr::logical_not(x), y, alg);
}
rewrite_implies(x, y, alg)
}
Expr::Equiv(a, b) => {
let x = rewrite(a, alg, opts);
let y = rewrite(b, alg, opts);
rewrite_equiv(x, y, alg)
}
}
}
fn ordered(a: Expr, b: Expr) -> (Expr, Expr) {
let ka = expr_key(&a);
let kb = expr_key(&b);
if kb < ka { (b, a) } else { (a, b) }
}
fn expr_key(e: &Expr) -> String {
e.to_infix()
}
fn rewrite_and(x: Expr, y: Expr, alg: Algebra) -> Expr {
if let (Expr::Const(a), Expr::Const(b)) = (&x, &y) {
return Expr::Const(alg.and(*a, *b));
}
if alg.is_poisonous() {
if x.is_unknown_const() || y.is_unknown_const() {
return Expr::Const(Ternary::Unknown);
}
if let Expr::Const(Ternary::True) = &x {
return y;
}
if let Expr::Const(Ternary::True) = &y {
return x;
}
let (a, b) = ordered(x, y);
return Expr::and(a, b);
}
if let Expr::Const(Ternary::False) = &x {
return Expr::Const(Ternary::False);
}
if let Expr::Const(Ternary::False) = &y {
return Expr::Const(Ternary::False);
}
if let Expr::Const(Ternary::True) = &x {
return y;
}
if let Expr::Const(Ternary::True) = &y {
return x;
}
if x == y {
return x;
}
if let Some(absorbed) = absorb(&x, &y) {
return absorbed;
}
if let Some(absorbed) = absorb(&y, &x) {
return absorbed;
}
let (a, b) = ordered(x, y);
Expr::and(a, b)
}
fn absorb(x: &Expr, y: &Expr) -> Option<Expr> {
if let Expr::Or(p, q) = y
&& (**p == *x || **q == *x)
{
return Some(x.clone());
}
None
}
fn absorb_or(x: &Expr, y: &Expr) -> Option<Expr> {
if let Expr::And(p, q) = y
&& (**p == *x || **q == *x)
{
return Some(x.clone());
}
None
}
fn rewrite_or(x: Expr, y: Expr, alg: Algebra) -> Expr {
if let (Expr::Const(a), Expr::Const(b)) = (&x, &y) {
return Expr::Const(alg.or(*a, *b));
}
if alg.is_poisonous() {
if x.is_unknown_const() || y.is_unknown_const() {
return Expr::Const(Ternary::Unknown);
}
if let Expr::Const(Ternary::False) = &x {
return y;
}
if let Expr::Const(Ternary::False) = &y {
return x;
}
let (a, b) = ordered(x, y);
return Expr::or(a, b);
}
if let Expr::Const(Ternary::True) = &x {
return Expr::Const(Ternary::True);
}
if let Expr::Const(Ternary::True) = &y {
return Expr::Const(Ternary::True);
}
if let Expr::Const(Ternary::False) = &x {
return y;
}
if let Expr::Const(Ternary::False) = &y {
return x;
}
if x == y {
return x;
}
if let Some(absorbed) = absorb_or(&x, &y) {
return absorbed;
}
if let Some(absorbed) = absorb_or(&y, &x) {
return absorbed;
}
let (a, b) = ordered(x, y);
Expr::or(a, b)
}
fn rewrite_xor(x: Expr, y: Expr, alg: Algebra) -> Expr {
if let (Expr::Const(a), Expr::Const(b)) = (&x, &y) {
return Expr::Const(alg.strong_add(*a, *b));
}
if alg != Algebra::Lukasiewicz && (x.is_unknown_const() || y.is_unknown_const()) {
return Expr::Const(Ternary::Unknown);
}
if alg == Algebra::Lukasiewicz && x.is_unknown_const() && y.is_unknown_const() {
return Expr::Const(Ternary::True);
}
if x == y && alg != Algebra::Lukasiewicz {
return x;
}
let (a, b) = ordered(x, y);
Expr::xor(a, b)
}
fn rewrite_implies(x: Expr, y: Expr, alg: Algebra) -> Expr {
if let (Expr::Const(a), Expr::Const(b)) = (&x, &y) {
return Expr::Const(alg.implies(*a, *b));
}
if alg != Algebra::Bochvar
&& let Expr::Const(Ternary::False) = &x
{
return Expr::Const(Ternary::True);
}
Expr::implies(x, y)
}
fn rewrite_equiv(x: Expr, y: Expr, alg: Algebra) -> Expr {
if let (Expr::Const(a), Expr::Const(b)) = (&x, &y) {
return Expr::Const(alg.equiv(*a, *b));
}
if x == y && alg == Algebra::Lukasiewicz {
return Expr::Const(Ternary::True);
}
Expr::equiv(x, y)
}
pub fn semantic_fingerprint(expr: &Expr, alg: Algebra) -> u64 {
let vars = expr.variables();
let Ok(tt) = truth_table(expr, alg) else {
return 0;
};
let mut hash = 1469598103934665603u64;
for v in &vars {
for b in v.as_bytes() {
hash ^= u64::from(*b);
hash = hash.wrapping_mul(1099511628211);
}
hash ^= 0xff;
hash = hash.wrapping_mul(1099511628211);
}
for val in &tt.values {
hash ^= u64::from(val.as_char() as u8);
hash = hash.wrapping_mul(1099511628211);
}
hash
}
pub fn semantically_equal(
a: &Expr,
b: Expr,
alg: Algebra,
) -> Result<bool, trit_core::limits::LimitError> {
Ok(crate::eval::equivalent(a, &b, alg)?.equivalent)
}
#[doc(hidden)]
pub fn unused_var_map() -> VarMap {
VarMap::new()
}
#[cfg(test)]
mod tests {
use super::*;
use trit_core::parser::Parser;
fn simp(src: &str, alg: Algebra) -> String {
let e = Parser::parse(src).unwrap();
simplify(&e, alg).to_infix()
}
#[test]
fn folds_constants() {
assert_eq!(simp("1 & 0", Algebra::Kleene), "0");
assert_eq!(simp("1 | 0", Algebra::Kleene), "1");
assert_eq!(simp("!1", Algebra::Kleene), "0");
}
#[test]
fn folds_semantic_constants() {
assert_eq!(simp("x | 1", Algebra::Kleene), "1");
assert_eq!(simp("x & 0", Algebra::Kleene), "0");
assert_eq!(simp("1 & 0 | 1", Algebra::Kleene), "1");
let e = Parser::parse("a & 1 | b & 0").unwrap();
assert_eq!(simplify(&e, Algebra::Kleene), Expr::Var("a".into()));
}
#[test]
fn applies_idempotence_and_identity() {
assert_eq!(simp("a & a", Algebra::Kleene), "a");
assert_eq!(simp("a | a", Algebra::Kleene), "a");
assert_eq!(simp("a & 1", Algebra::Kleene), "a");
assert_eq!(simp("a | 0", Algebra::Kleene), "a");
}
#[test]
fn bochvar_poison_differs_from_kleene() {
assert_eq!(simp("0 & x", Algebra::Bochvar), "X");
assert_eq!(simp("0 & x", Algebra::Kleene), "0");
}
#[test]
fn simplification_preserves_semantics() {
for src in [
"a & b | a",
"!(a & b)",
"(a => b) & (b => a)",
"a ^ b",
"a <=> b",
"a & 1 | b & 0",
] {
let e = Parser::parse(src).unwrap();
let s = simplify(&e, Algebra::Kleene);
let r = crate::eval::equivalent(&e, &s, Algebra::Kleene).unwrap();
assert!(r.equivalent, "simplify changed semantics: {src} -> {s}");
}
}
#[test]
fn simplification_preserves_semantics_in_bochvar() {
for src in ["a & b | a", "0 & x", "a | 1", "a & x"] {
let e = Parser::parse(src).unwrap();
let s = simplify(&e, Algebra::Bochvar);
let r = crate::eval::equivalent(&e, &s, Algebra::Bochvar).unwrap();
assert!(
r.equivalent,
"bochvar simplify changed semantics: {src} -> {s}"
);
}
}
#[test]
fn absorption_only_removes_matching_operator() {
let e = Parser::parse("a | (a | b)").unwrap();
let s = simplify(&e, Algebra::Kleene);
assert!(
crate::eval::equivalent(&e, &s, Algebra::Kleene)
.unwrap()
.equivalent,
"a | (a | b) -> {s}"
);
assert_eq!(simp("a | (a & b)", Algebra::Kleene), "a");
}
#[test]
fn xor_and_equiv_self_respect_unknown() {
assert_eq!(simp("a ^ a", Algebra::Kleene), "a");
assert_eq!(simp("a ^ a", Algebra::Bochvar), "a");
let e = Parser::parse("a <=> a").unwrap();
let s = simplify(&e, Algebra::Kleene);
assert!(
crate::eval::equivalent(&e, &s, Algebra::Kleene)
.unwrap()
.equivalent,
"a ≡ a -> {s}"
);
assert_eq!(simp("a <=> a", Algebra::Lukasiewicz), "1");
}
#[test]
fn semantically_equal_uses_real_equivalence() {
let a = Parser::parse("a & b").unwrap();
let b = Parser::parse("b & a").unwrap();
assert!(semantically_equal(&a, b, Algebra::Kleene).unwrap());
let a = Parser::parse("a").unwrap();
let b = Parser::parse("a & (b | 1)").unwrap();
assert!(semantically_equal(&a, b, Algebra::Kleene).unwrap());
let a = Parser::parse("a | (a | b)").unwrap();
let b = Parser::parse("a").unwrap();
assert!(!semantically_equal(&a, b, Algebra::Kleene).unwrap());
}
}