commit 096e2105ddee577a7980a2e056f827143c98a110 Author: Martin Berg Alstad Date: Wed Jun 5 20:41:00 2024 +0200 Initial commit diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..528c26c --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,20 @@ +name: Rust + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build --verbose + - name: Run tests + run: cargo test --verbose \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..b58b603 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c446f0d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/simplify_truths.iml b/.idea/simplify_truths.iml new file mode 100644 index 0000000..cf84ae4 --- /dev/null +++ b/.idea/simplify_truths.iml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..52e5c75 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,32 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "simplify_truths" +version = "0.1.0" +dependencies = [ + "nom", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..85986ee --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "simplify_truths" +version = "0.1.0" +edition = "2021" +authors = ["Martin Berg Alstad"] + +[dependencies] +nom = "7.1.3" diff --git a/src/expressions/expression.rs b/src/expressions/expression.rs new file mode 100644 index 0000000..9886165 --- /dev/null +++ b/src/expressions/expression.rs @@ -0,0 +1,194 @@ +use std::fmt::Display; + +use crate::expressions::operator::BinaryOperator; +use crate::parsing::expression_parser::parse_expression; + +pub trait OppositeEq { + fn opposite_eq(&self, other: &Self) -> bool; +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Expression { + Not(Box), + Binary(Box, BinaryOperator, Box), + Atomic(String), +} + +impl Expression { + pub fn is_atomic(&self) -> bool { + match self { + Expression::Not(expr) => expr.is_atomic(), + Expression::Binary(_, _, _) => false, + Expression::Atomic(_) => true + } + } + + pub fn is_not(&self) -> bool { + matches!(self, Expression::Not(_)) + } + + pub fn exists(&self, atomic_value: &str) -> bool { + match self { + Expression::Not(expr) => expr.exists(atomic_value), + Expression::Binary(left, _, right) => left.exists(atomic_value) || right.exists(atomic_value), + Expression::Atomic(value) => value == atomic_value, + } + } +} + +impl OppositeEq for Expression { + fn opposite_eq(&self, other: &Self) -> bool { + match (self, other) { + (Expression::Not(_), Expression::Not(_)) => false, + (Expression::Not(_), _) => true, + (_, Expression::Not(_)) => true, + _ => false, + } + } +} + +impl<'a> TryFrom<&'a str> for Expression { + type Error = nom::Err>; + fn try_from(value: &'a str) -> Result { + parse_expression(value) + } +} + +impl TryFrom for Expression { + type Error = nom::Err>; + fn try_from(value: String) -> Result { + Self::try_from(value.as_str()) + .map_err(|err| err.map(|err| nom::error::Error::new(err.input.into(), err.code))) + } +} + +impl Display for Expression { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Expression::Not(expr) if expr.is_atomic() => write!(f, "¬{expr}"), + Expression::Not(expr) => write!(f, "¬({expr})"), + Expression::Binary(left, BinaryOperator::And, right) => { + write!(f, "{left} ⋀ {right}") + } + // TODO do not use parentheses on root level or if several operators are on the same level + Expression::Binary(left, BinaryOperator::Or, right) => { + write!(f, "({left} ⋁ {right})") + } + Expression::Binary(left, BinaryOperator::Implication, right) => { + write!(f, "{left} ➔ {right}") + } + Expression::Atomic(value) => write!(f, "{value}"), + } + } +} + +#[cfg(test)] +mod tests { + use crate::{and, atomic, implies, not, or}; + use crate::expressions::expression::Expression; + + #[test] + fn test_expression_a_and_not_b_display() { + let expression = and!( + atomic!("a"), + not!(atomic!("b")) + ); + assert_eq!(expression.to_string(), "a ⋀ ¬b"); + } + + #[test] + #[ignore] + fn test_expression_a_or_b_and_c_display() { + // TODO + let expression = or!( + atomic!("a"), + and!( + atomic!("b"), + atomic!("c") + )); + assert_eq!(expression.to_string(), "a ⋁ b ⋀ c"); + } + + #[test] + fn test_expression_c_and_a_or_b_display() { + let expression = and!( + or!( + atomic!("a"), + atomic!("b") + ), + atomic!("c") + ); + assert_eq!(expression.to_string(), "(a ⋁ b) ⋀ c"); + } + + #[test] + fn test_expression_a_implies_b_display() { + let expression = implies!( + atomic!("a"), + atomic!("b") + ); + assert_eq!(expression.to_string(), "a ➔ b"); + } + + #[test] + fn test_expression_not_a_and_b_display() { + let expression = not!(and!( + atomic!("a"), + atomic!("b") + )); + assert_eq!(expression.to_string(), "¬(a ⋀ b)"); + } + + #[test] + fn test_from_str_into_expression_atomic() { + let expression: Expression = "a".try_into().unwrap(); + assert_eq!(expression, atomic!("a")); + } + + #[test] + fn test_from_str_into_expression_not() { + let expression: Expression = "!a".try_into().unwrap(); + assert_eq!(expression, not!(atomic!("a"))); + } + + #[test] + fn test_from_str_into_expression_and() { + let expression: Expression = "a & b".try_into().unwrap(); + assert_eq!(expression, and!(atomic!("a"), atomic!("b"))); + } + + #[test] + fn test_from_str_into_expression_or() { + let expression: Expression = "a | b".try_into().unwrap(); + assert_eq!(expression, or!(atomic!("a"), atomic!("b"))); + } + + #[test] + fn test_from_str_into_expression_implies() { + let expression: Expression = "a => b".try_into().unwrap(); + assert_eq!(expression, implies!(atomic!("a"), atomic!("b"))); + } + + #[test] + fn test_from_str_into_expression_complex() { + let expression: Expression = "a & b | c".try_into().unwrap(); + assert_eq!(expression, or!(and!(atomic!("a"), atomic!("b")), atomic!("c"))); + } + + #[test] + fn test_from_str_into_expression_complex_parentheses() { + let expression: Expression = "a & (b | c)".try_into().unwrap(); + assert_eq!(expression, and!(atomic!("a"), or!(atomic!("b"), atomic!("c")))); + } + + #[test] + fn test_from_str_into_expression_very_complex_parentheses() { + let expression: Expression = "(a & b) | c => (d & e)".try_into().unwrap(); + assert_eq!(expression, implies!(or!(and!(atomic!("a"), atomic!("b")), atomic!("c")), and!(atomic!("d"), atomic!("e")))); + } + + #[test] + fn test_from_str_into_expression_empty() { + assert!(Expression::try_from("").is_err()); + } +} diff --git a/src/expressions/helpers.rs b/src/expressions/helpers.rs new file mode 100644 index 0000000..e794146 --- /dev/null +++ b/src/expressions/helpers.rs @@ -0,0 +1,93 @@ +#[macro_export] +macro_rules! and { + ($left:expr, $right:expr) => { + $crate::binary!($left, $crate::expressions::operator::BinaryOperator::And, $right) + }; +} + +#[macro_export] +macro_rules! or { + ($left:expr, $right:expr) => { + $crate::binary!($left, $crate::expressions::operator::BinaryOperator::Or, $right) + }; +} + +#[macro_export] +macro_rules! implies { + ($left:expr, $right:expr) => { + $crate::binary!($left, $crate::expressions::operator::BinaryOperator::Implication, $right) + }; +} + +#[macro_export] +macro_rules! binary { + ($left:expr, $op:expr, $right:expr) => { + $crate::expressions::expression::Expression::Binary(Box::new($left), $op, Box::new($right)) + }; +} + +#[macro_export] +macro_rules! not { + ($value:expr) => { + $crate::expressions::expression::Expression::Not(Box::new($value)) + }; +} + +#[macro_export] +macro_rules! atomic { + ($value:expr) => { + $crate::expressions::expression::Expression::Atomic($value.to_string()) + }; +} + +// TODO +#[macro_export] +macro_rules! eval { + ($a:literal && $b:literal) => { + $crate::and!($crate::eval!($a), $crate::eval!($b)) + }; + ($a:literal || $b:literal) => { + $crate::or!($crate::eval!($a), $crate::eval!($b)) + }; + ($a:literal => $b:literal) => { + $crate::implies!($crate::eval!($a), $crate::eval!($b)) + }; + (!$a:expr) => { + $crate::not!($crate::eval!($a)) + }; + ($value:expr) => { + $crate::atomic!($value) + }; +} + +#[cfg(test)] +mod tests { + use crate::eval; + use crate::expressions::expression::Expression::{Atomic, Binary, Not}; + use crate::expressions::operator::BinaryOperator::{And, Implication, Or}; + + #[test] + fn eval_atomic() { + assert_eq!(eval!("a"), Atomic("a".to_string())); + } + + #[test] + fn eval_not() { + assert_eq!(eval!(!"a"), Not(Box::new(Atomic("a".to_string())))); + } + + #[test] + fn eval_and() { + assert_eq!(eval!("a" && "b"), Binary(Box::new(Atomic("a".to_string())), And, Box::new(Atomic("b".to_string())))); + } + + #[test] + fn eval_or() { + assert_eq!(eval!("a" || "b"), Binary(Box::new(Atomic("a".to_string())), Or, Box::new(Atomic("b".to_string())))); + } + + #[test] + fn eval_implies() { + assert_eq!(eval!("a" => "b"), Binary(Box::new(Atomic("a".to_string())), Implication, Box::new(Atomic("b".to_string())))); + } +} diff --git a/src/expressions/mod.rs b/src/expressions/mod.rs new file mode 100644 index 0000000..afe4cfc --- /dev/null +++ b/src/expressions/mod.rs @@ -0,0 +1,5 @@ +pub mod expression; +pub mod operator; +#[macro_use] +pub mod helpers; +pub mod simplify; \ No newline at end of file diff --git a/src/expressions/operator.rs b/src/expressions/operator.rs new file mode 100644 index 0000000..f3e8c4f --- /dev/null +++ b/src/expressions/operator.rs @@ -0,0 +1,17 @@ +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum BinaryOperator { + Implication, + Or, + And, +} + +impl From for &str { + fn from(op: BinaryOperator) -> Self { + match op { + BinaryOperator::Implication => "=>", + BinaryOperator::Or => "|", + BinaryOperator::And => "&", + } + } +} + diff --git a/src/expressions/simplify.rs b/src/expressions/simplify.rs new file mode 100644 index 0000000..bde8166 --- /dev/null +++ b/src/expressions/simplify.rs @@ -0,0 +1,352 @@ +use crate::expressions::expression::{Expression, OppositeEq}; +use crate::expressions::operator::BinaryOperator; + +pub trait Simplify { + fn elimination_of_implication(&self) -> Self; + fn double_negation_elimination(&self) -> Self; + fn de_morgans_laws(&self) -> Self; + fn absorption_law(&self) -> Self; + fn associative_law(&self) -> Self; + fn distribution_law(&self) -> Self; + fn commutative_law(&self) -> Self; +} + +impl Simplify for Expression { + /// Eliminate the implication operator from the expression. + /// This is done by replacing `a ➔ b` with `¬a ⋁ b`. + fn elimination_of_implication(&self) -> Self { + match self { + Expression::Not(expr) => not!(expr.elimination_of_implication()), + Expression::Binary(left, BinaryOperator::Implication, right) => { + let left = left.elimination_of_implication(); + let right = right.elimination_of_implication(); + or!(not!(left), right) + } + Expression::Binary(left, operator, right) => { + let left = left.elimination_of_implication(); + let right = right.elimination_of_implication(); + binary!(left, *operator, right) + } + atomic @ Expression::Atomic(_) => atomic.clone(), + } + } + + /// Eliminate double negations from the expression. + /// This is done by replacing `¬¬a` with `a`. + /// This function is recursive and will continue to eliminate double negations until none are left. + fn double_negation_elimination(&self) -> Self { + match self { + Expression::Not(expr) => { + if let Expression::Not(inner) = *expr.clone() { + inner.double_negation_elimination() + } else { + not!(expr.double_negation_elimination()) + } + } + Expression::Binary(left, operator, right) => { + let left = left.double_negation_elimination(); + let right = right.double_negation_elimination(); + binary!(left, *operator, right) + } + atomic @ Expression::Atomic(_) => atomic.clone(), + } + } + + fn de_morgans_laws(&self) -> Self { + match self { + Expression::Not(expr) => { + match *expr.clone() { + Expression::Binary(left, BinaryOperator::And, right) => { + // TODO unnecessary cloning calls to de_morgans_laws? + let left = not!(left.de_morgans_laws()); + let right = not!(right.de_morgans_laws()); + or!(left, right).de_morgans_laws() + } + Expression::Binary(left, BinaryOperator::Or, right) => { + let left = not!(left.de_morgans_laws()); + let right = not!(right.de_morgans_laws()); + and!(left, right).de_morgans_laws() + } + _ => not!(expr.de_morgans_laws()), + } + } + Expression::Binary(left, operator, right) => { + let left = left.de_morgans_laws(); + let right = right.de_morgans_laws(); + binary!(left, *operator, right) + } + atomic @ Expression::Atomic(_) => atomic.clone(), + } + } + + // TODO deduplicate code + fn absorption_law(&self) -> Self { + match self { + Expression::Binary(left, BinaryOperator::And, right) => { + let (left_ref, right_ref) = (left.as_ref(), right.as_ref()); + match (left_ref, right_ref) { + (_, Expression::Binary(right_left, BinaryOperator::Or, right_right)) => { + if left_ref == right_left.as_ref() || left_ref == right_right.as_ref() { + return left.absorption_law(); + } else if right_left.is_atomic() && right_right.is_atomic() && left.opposite_eq(right_left) { + if left.opposite_eq(right_left) { + return and!(left.absorption_law(), right_left.absorption_law()); + } else if left.opposite_eq(right_right) { + return and!(left.absorption_law(), right_right.absorption_law()); + } + } + and!(left.absorption_law(), right.absorption_law()) + } + (Expression::Binary(left_left, BinaryOperator::Or, left_right), _) => { + if right_ref == left_left.as_ref() || right_ref == left_right.as_ref() { + return right.absorption_law(); + } else if left_left.is_atomic() && left_right.is_atomic() && right.opposite_eq(left_left) { + if right.opposite_eq(left_left) { + return and!(left_right.absorption_law(), right.absorption_law()); + } else if right.opposite_eq(left_right) { + return and!(left_left.absorption_law(), right.absorption_law()); + } + } + and!(left.absorption_law(), right.absorption_law()) + } + (left, right) => and!(left.absorption_law(), right.absorption_law()) + } + } + Expression::Binary(left, BinaryOperator::Or, right) => { + let (left_ref, right_ref) = (left.as_ref(), right.as_ref()); + match (left_ref, right_ref) { + (_, Expression::Binary(right_left, BinaryOperator::And, right_right)) => { + if left_ref == right_left.as_ref() || left_ref == right_right.as_ref() { + return left.absorption_law(); + } else if right_left.is_atomic() && right_right.is_atomic() && left.opposite_eq(right_left) { + if left.opposite_eq(right_left) { + return or!(left.absorption_law(), right_left.absorption_law()); + } else if left.opposite_eq(right_right) { + return or!(left.absorption_law(), right_right.absorption_law()); + } + } + or!(left.absorption_law(), right.absorption_law()) + } + (Expression::Binary(left_left, BinaryOperator::And, left_right), _) => { + if right_ref == left_left.as_ref() || right_ref == left_right.as_ref() { + return right.absorption_law(); + } else if left_left.is_atomic() && left_right.is_atomic() && right.opposite_eq(left_left) { + if right.opposite_eq(left_left) { + return or!(left_right.absorption_law(), right.absorption_law()); + } else if right.opposite_eq(left_right) { + return or!(left_left.absorption_law(), right.absorption_law()); + } + } + or!(left.absorption_law(), right.absorption_law()) + } + (left, right) => or!(left.absorption_law(), right.absorption_law()) + } + } + Expression::Binary(left, operator, right) => { + let left = left.absorption_law(); + let right = right.absorption_law(); + binary!(left, *operator, right) + } + Expression::Not(expr) => not!(expr.absorption_law()), + atomic => atomic.clone(), + } + } + + fn associative_law(&self) -> Self { + todo!("? | Associative law: (a ⋀ b) ⋀ c == a ⋀ (b ⋀ c) and (a ⋁ b) ⋁ c == a ⋁ (b ⋁ c)") + } + + // TODO deduplicate code + fn distribution_law(&self) -> Self { + match self { + Expression::Binary(left, BinaryOperator::And, right) => { + match (left.as_ref(), right.as_ref()) { + (Expression::Atomic(_), Expression::Binary(right_left, BinaryOperator::Or, right_right)) => { + let right_left = right_left.distribution_law(); + let right_right = right_right.distribution_law(); + or!(and!(*left.clone(), right_left), and!(*left.clone(), right_right)) + } + (Expression::Binary(left_left, BinaryOperator::Or, left_right), Expression::Atomic(_)) => { + let left_left = left_left.distribution_law(); + let left_right = left_right.distribution_law(); + or!(and!(left_left, *right.clone()), and!(left_right, *right.clone())) + } + (left, right) => and!(left.distribution_law(), right.distribution_law()) + } + } + Expression::Binary(left, BinaryOperator::Or, right) => { + match (left.as_ref(), right.as_ref()) { + (Expression::Atomic(_), Expression::Binary(right_left, BinaryOperator::And, right_right)) => { + let right_left = right_left.distribution_law(); + let right_right = right_right.distribution_law(); + and!(or!(*left.clone(), right_left), or!(*left.clone(), right_right)) + } + (Expression::Binary(left_left, BinaryOperator::And, left_right), Expression::Atomic(_)) => { + let left_left = left_left.distribution_law(); + let left_right = left_right.distribution_law(); + and!(or!(left_left, *right.clone()), or!(left_right, *right.clone())) + } + (left, right) => or!(left.distribution_law(), right.distribution_law()) + } + } + Expression::Binary(left, operator, right) => { + let left = left.distribution_law(); + let right = right.distribution_law(); + binary!(left, *operator, right) + } + Expression::Not(expr) => expr.distribution_law(), + atomic => atomic.clone(), + } + } + + fn commutative_law(&self) -> Self { + todo!("? | Order of operands does not matter in AND and OR operations.") + } +} + +#[cfg(test)] +mod tests { + use crate::expressions::simplify::Simplify; + + #[test] + fn test_elimination_of_implication() { + let expression = eval!("a" => "b").elimination_of_implication(); + assert_eq!(expression, or!(not!(atomic!("a")), atomic!("b"))); + } + + #[test] + fn test_elimination_of_implication_nested() { + let expression = implies!(atomic!("a"), implies!(atomic!("b"), atomic!("c"))).elimination_of_implication(); + assert_eq!(expression, or!(not!(atomic!("a")), or!(not!(atomic!("b")), atomic!("c")))); + } + + #[test] + fn test_elimination_of_implication_none() { + let expression = eval!("a" && "b").elimination_of_implication(); + assert_eq!(expression, eval!("a" && "b")); + } + + #[test] + fn test_elimination_of_implication_nested_none() { + let expression = or!(atomic!("a"), and!(atomic!("b"), atomic!("c"))).elimination_of_implication(); + assert_eq!(expression, or!(atomic!("a"), and!(atomic!("b"), atomic!("c")))); + } + + #[test] + fn test_double_negation_elimination() { + let expression = not!(not!(atomic!("a"))).double_negation_elimination(); + assert_eq!(expression, atomic!("a")); + } + + #[test] + fn test_triple_negation_elimination() { + let expression = not!(not!(not!(atomic!("a")))).double_negation_elimination(); + assert_eq!(expression, not!(atomic!("a"))); + } + + #[test] + fn test_five_negation_elimination() { + let expression = not!(not!(not!(not!(not!(atomic!("a")))))).double_negation_elimination(); + assert_eq!(expression, not!(atomic!("a"))); + } + + #[test] + fn test_no_negation_elimination() { + let expression = atomic!("a").double_negation_elimination(); + assert_eq!(expression, atomic!("a")); + } + + #[test] + fn test_double_negation_nested_elimination() { + let expression = and!(or!(not!(eval!(!"a")), eval!("b")), not!(eval!(!"c"))).double_negation_elimination(); + assert_eq!(expression, and!(or!(atomic!("a"), atomic!("b")), atomic!("c"))); + } + + #[test] + fn test_de_morgans_laws_and() { + let expression = not!(eval!("a" && "b")).de_morgans_laws(); + assert_eq!(expression, or!(not!(atomic!("a")), not!(atomic!("b")))); + } + + #[test] + fn test_de_morgans_laws_or() { + let expression = not!(eval!("a" || "b")).de_morgans_laws(); + assert_eq!(expression, and!(not!(atomic!("a")), not!(atomic!("b")))); + } + + #[test] + fn test_de_morgans_laws_nested_or() { + let expression = not!(or!(eval!("a" && "b"), atomic!("c"))).de_morgans_laws(); // ¬(a ⋀ b ⋁ c) + assert_eq!(expression, and!(or!(eval!(!"a"), eval!(!"b")), eval!(!"c"))); // ¬(a ⋀ b) ⋀ ¬c == (¬a ⋁ ¬b) ⋀ ¬c + } + + #[test] + fn test_de_morgans_laws_nested_and() { + let expression = not!(and!(eval!("a" || "b"), atomic!("c"))).de_morgans_laws(); // ¬(a ⋁ b ⋀ c) + assert_eq!(expression, or!(and!(eval!(!"a"), eval!(!"b")), eval!(!"c"))); // ¬(a ⋁ b) ⋀ ¬c == (¬a ⋀ ¬b) ⋁ ¬c + } + + #[test] + fn test_de_morgans_laws_nested_and_or() { + let expression = not!(and!(eval!("a" || "b"), or!(atomic!("c"), atomic!("d")))).de_morgans_laws(); // ¬(a ⋁ b ⋀ c ⋁ d) + assert_eq!(expression, or!(and!(eval!(!"a"), eval!(!"b")), and!(eval!(!"c"), eval!(!"d")))); // ¬(a ⋁ b) ⋀ ¬(c ⋁ d) == (¬a ⋀ ¬b) ⋁ (¬c ⋀ ¬d) + } + + #[test] + fn test_absorption_law_and() { + let expression = and!(atomic!("a"), eval!("a" || "b")).absorption_law(); + assert_eq!(expression, atomic!("a")); + } + + #[test] + fn test_absorption_law_or() { + let expression = or!(atomic!("a"), eval!("a" && "b")).absorption_law(); + assert_eq!(expression, atomic!("a")); + } + + #[test] + fn test_absorption_law_nested_and() { + let expression = and!(atomic!("a"), or!(atomic!("a"), atomic!("b"))).absorption_law(); + assert_eq!(expression, atomic!("a")); + } + + // !A & B | A <=> B | A + #[test] + fn test_absorption_law_not() { + let expression = or!(and!(not!(atomic!("a")), atomic!("b")), atomic!("a")).absorption_law(); + assert_eq!(expression, or!(atomic!("b"), atomic!("a"))); + } + + // A & B | !A <=> B | !A + #[test] + fn test_absorption_law_not_reversed() { + let expression = or!(and!(atomic!("a"), atomic!("b")), not!(atomic!("a"))).absorption_law(); + assert_eq!(expression, or!(atomic!("b"), not!(atomic!("a")))); + } + + // !A & B | !A <=> !A + #[test] + fn test_absorption_law_double_not() { + let expression = or!(and!(not!(atomic!("a")), atomic!("b")), not!(atomic!("a"))).absorption_law(); + assert_eq!(expression, not!(atomic!("a"))); + } + + // (A | B) & !A <=> B & !A + #[test] + fn test_in_parenthesis() { + let expression = and!(or!(atomic!("a"), atomic!("b")), not!(atomic!("a"))).absorption_law(); + assert_eq!(expression, and!(atomic!("b"), not!(atomic!("a")))); + } + + #[test] + fn test_distributive_law_and() { + let expression = and!(atomic!("a"), or!(atomic!("b"), atomic!("c"))).distribution_law(); + assert_eq!(expression, or!(and!(atomic!("a"), atomic!("b")), and!(atomic!("a"), atomic!("c")))); + } + + #[test] + fn test_distributive_law_or() { + let expression = or!(atomic!("a"), and!(atomic!("b"), atomic!("c"))).distribution_law(); + assert_eq!(expression, and!(or!(atomic!("a"), atomic!("b")), or!(atomic!("a"), atomic!("c")))); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d514e6c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,6 @@ +mod expressions; +mod parsing; + +fn main() { + println!("Hello, world!"); +} diff --git a/src/parsing/expression_parser.rs b/src/parsing/expression_parser.rs new file mode 100644 index 0000000..0227f6c --- /dev/null +++ b/src/parsing/expression_parser.rs @@ -0,0 +1,351 @@ +use nom::branch::alt; +use nom::bytes::complete::{tag, take_while, take_while1}; +use nom::character::complete::char; +use nom::combinator::opt; +use nom::error::Error; +use nom::IResult; +use nom::sequence::{pair, preceded}; + +use crate::{and, atomic, implies, not, or}; +use crate::expressions::expression::Expression; +use crate::parsing::utils::{exhausted, IntoResult, parenthesized, trim}; + +pub fn parse_expression(input: &str) -> Result>> { + exhausted(_parse_expression)(input).into_result() +} + +fn _parse_expression(input: &str) -> IResult<&str, Expression> { + let (remaining, atomic_expression) = left_hand_side(input)?; + if let (remaining, Some(complex_expression)) = opt(expression(atomic_expression.clone()))(remaining)? { + Ok((remaining, complex_expression)) + } else { + Ok((remaining, atomic_expression)) + } +} + +fn left_hand_side(input: &str) -> IResult<&str, Expression> { + alt(( + value, + not, + parenthesized(complete_expression) + ))(input) +} + +fn expression<'a>(previous: Expression) -> impl Fn(&'a str) -> IResult<&'a str, Expression> { + move |input: &'a str| { + let (remaining, new) = operator_combinators(previous.clone())(input)?; + if !remaining.is_empty() { + expression(new.clone())(remaining) + } else { + Ok((remaining, new)) + } + } +} + +fn operator_combinators(expression: Expression) -> impl Fn(&str) -> IResult<&str, Expression> { + move |input: &str| { + alt(( + implies(expression.clone()), + or(expression.clone()), + and(expression.clone()), + not, + ))(input) + } +} + +fn complete_expression(input: &str) -> IResult<&str, Expression> { + let (remaining, atomic) = left_hand_side(input)?; + operator_combinators(atomic.clone())(remaining) +} + +fn and<'a>(previous: Expression) -> impl Fn(&'a str) -> IResult<&'a str, Expression> { + move |input: &'a str| { + preceded( + trim(char('&')), + left_hand_side, + )(input).map(|(remaining, right)| { + (remaining, and!(previous.clone(), right)) + }) + } +} + +fn complete_and(input: &str) -> IResult<&str, Expression> { + let (remaining, atomic) = value(input)?; + and(atomic.clone())(remaining) +} + +fn or<'a>(previous: Expression) -> impl Fn(&'a str) -> IResult<&'a str, Expression> { + move |input: &'a str| { + preceded( + trim(char('|')), + alt(( + complete_and, + left_hand_side, + )), + )(input).map(|(remaining, right)| { + (remaining, or!(previous.clone(), right)) + }) + } +} + +fn complete_or(input: &str) -> IResult<&str, Expression> { + let (remaining, atomic) = value(input)?; + or(atomic.clone())(remaining) +} + +fn implies<'a>(previous: Expression) -> impl Fn(&'a str) -> IResult<&'a str, Expression> { + move |input: &'a str| { + preceded( + trim(tag("=>")), + alt(( + complete_and, + complete_or, + left_hand_side, + )), + )(input).map(|(remaining, right)| { + (remaining, implies!(previous.clone(), right)) + }) + } +} + +fn not(input: &str) -> IResult<&str, Expression> { + preceded( + char('!'), + left_hand_side, + )(input).map(|(remaining, right)| { + (remaining, not!(right)) + }) +} + +fn value(input: &str) -> IResult<&str, Expression> { + pair( + take_while1(|c: char| c.is_ascii_alphabetic()), + take_while(|c: char| c.is_ascii_alphanumeric() || c == '_'), + )(input) + .map(|(remaining, (first, rest))| { + let value = format!("{first}{rest}"); + (remaining, atomic!(value)) + }) +} + +#[cfg(test)] +mod tests { + use crate::{and, atomic, implies, not, or}; + + #[test] + fn test_parse() { + let input = "a & b => c"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(implies!(and!(atomic!("a"), atomic!("b")), atomic!("c")))); + } + + #[test] + fn test_parse_complex() { + let input = "a => b | !(!c | d & e) => b"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(implies!( + implies!( + atomic!("a"), + or!( + atomic!("b"), + not!( + or!( + not!(atomic!("c")), + and!(atomic!("d"), atomic!("e")) + ) + ) + ) + ), + atomic!("b") + ))); + } + + #[test] + fn test_operator_weight() { + let input = "A & B | C => D | E & F"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(implies!(or!(and!(atomic!("A"), atomic!("B")), atomic!("C")), or!(atomic!("D"), and!(atomic!("E"), atomic!("F")))))); + } + + #[test] + fn test_implies_chain() { + let input = "a => b => c"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(implies!(implies!(atomic!("a"), atomic!("b")), atomic!("c")))); + } + + #[test] + fn test_parse_parentheses() { + let input = "a & (b => c)"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(and!(atomic!("a"), implies!(atomic!("b"), atomic!("c"))))); + } + + #[test] + fn test_parse_not() { + let input = "!a"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(not!(atomic!("a")))); + } + + #[test] + fn test_parse_not_parentheses() { + let input = "!(a & b)"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(not!(and!(atomic!("a"), atomic!("b"))))); + } + + #[test] + fn test_expression_with_not_inside_and() { + let input = "a & !b"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(and!(atomic!("a"), not!(atomic!("b"))))); + } + + #[test] + fn test_expression_with_not_inside_or() { + let input = "a | !b"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(or!(atomic!("a"), not!(atomic!("b"))))); + } + + #[test] + fn test_expression_with_not_inside_implies() { + let input = "a => !b"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(implies!(atomic!("a"), not!(atomic!("b"))))); + } + + #[test] + fn test_expression_with_not_inside_parentheses() { + let input = "a & !(b | c)"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(and!(atomic!("a"), not!(or!(atomic!("b"), atomic!("c")))))); + } + + #[test] + fn test_even_not() { + let input = "!!!!a"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(not!(not!(not!(not!(atomic!("a"))))))); + } + + #[test] + fn test_odd_not() { + let input = "!!!!!a"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(not!(not!(not!(not!(not!(atomic!("a")))))))); + } + + #[test] + fn test_atomic() { + let input = "a"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(atomic!("a"))); + } + + #[test] + fn test_atomic_with_underscore() { + let input = "a_b"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(atomic!("a_b"))); + } + + #[test] + fn test_atomic_with_digits() { + let input = "a1"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(atomic!("a1"))); + } + + #[test] + fn test_empty() { + let input = ""; + let result = super::parse_expression(input); + assert!(result.is_err()); + } + + #[test] + fn test_or_chain() { + let input = "a | b | c | d | e | f | g"; + let result = super::parse_expression(input); + assert_eq!(result, Ok(or!(or!(or!(or!(or!(or!(atomic!("a"), atomic!("b")), atomic!("c")), atomic!("d")), atomic!("e")), atomic!("f")), atomic!("g")))); + } + + #[test] + fn test_expression() { + let input = "a"; + let result = super::_parse_expression(input); + assert_eq!(result, Ok(("", atomic!("a")))); + } + + #[test] + fn test_expression_and() { + let expression = atomic!("a"); + let input = " & b"; + let result = super::expression(expression)(input); + assert_eq!(result, Ok(("", and!(atomic!("a"), atomic!("b"))))); + } + + #[test] + fn test_expression_and_or() { + let expression = atomic!("a"); + let input = " & b | c"; + let result = super::expression(expression)(input); + assert_eq!(result, Ok(("", or!(and!(atomic!("a"), atomic!("b")), atomic!("c"))))); + } + + #[test] + fn test_expression_and_or_implies() { + let expression = atomic!("a"); + let input = " & b | c => d"; + let result = super::expression(expression)(input); + assert_eq!(result, Ok(("", implies!(or!(and!(atomic!("a"), atomic!("b")), atomic!("c")), atomic!("d"))))); + } + + #[test] + fn test_expression_parentheses_or() { + let expression = atomic!("a"); + let input = " & (b | c) => d"; + let result = super::expression(expression)(input); + assert_eq!(result, Ok(("", implies!(and!(atomic!("a"), or!(atomic!("b"), atomic!("c"))), atomic!("d"))))); + } + + #[test] + fn test_expression_parentheses_and() { + let input = "(a & b) | (c & d)"; + let result = super::_parse_expression(input); + assert_eq!(result, Ok(("", or!(and!(atomic!("a"), atomic!("b")), and!(atomic!("c"), atomic!("d")))))); + } + + #[test] + fn test_expression_parentheses_implies() { + let expression = atomic!("a"); + let input = " & b | (c => d)"; + let result = super::expression(expression)(input); + assert_eq!(result, Ok(("", or!(and!(atomic!("a"), atomic!("b")), implies!(atomic!("c"), atomic!("d")))))); + } + + #[test] + fn test_expression_nested_parentheses() { + let expression = atomic!("a"); + let input = " & (b | (c => d))"; + let result = super::expression(expression)(input); + assert_eq!(result, Ok(("", and!(atomic!("a"), or!(atomic!("b"), implies!(atomic!("c"), atomic!("d"))))))); + } + + #[test] + fn test_parse_or() { + let expression = atomic!("a"); + let input = " | b"; + let result = super::or(expression)(input); + assert_eq!(result, Ok(("", or!(atomic!("a"), atomic!("b"))))); + } + + #[test] + fn test_parse_or_parentheses() { + let input = "(a | b)"; + let result = super::_parse_expression(input); + assert_eq!(result, Ok(("", or!(atomic!("a"), atomic!("b"))))); + } +} diff --git a/src/parsing/mod.rs b/src/parsing/mod.rs new file mode 100644 index 0000000..c3e1d28 --- /dev/null +++ b/src/parsing/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod expression_parser; +mod utils; \ No newline at end of file diff --git a/src/parsing/utils.rs b/src/parsing/utils.rs new file mode 100644 index 0000000..f3e6cf6 --- /dev/null +++ b/src/parsing/utils.rs @@ -0,0 +1,101 @@ +use std::ops::RangeFrom; + +use nom::{InputIter, InputLength, InputTake, IResult, Slice}; +use nom::bytes::complete::take_while_m_n; +use nom::character::complete::{char, multispace0}; +use nom::combinator::eof; +use nom::error::{Error, ParseError}; +use nom::sequence::{delimited, terminated}; + +/// Trim leading and trailing whitespace from the input Parser +/// - Parameters +/// - `inner`: The parser to trim +/// - Returns: A parser that trims leading and trailing whitespace from the input and then runs the value from the inner parser +pub fn trim<'a, Parser, R>(inner: Parser) -> impl FnMut(&'a str) -> IResult<&'a str, R> + where + Parser: Fn(&'a str) -> IResult<&'a str, R> { + delimited( + multispace0, + inner, + multispace0, + ) +} + +/// Parse a parenthesized expression. This parser will parse an expression that is surrounded by parentheses +/// and will trim the whitespace surrounding the expression. +/// - Parameters +/// - `inner`: The parser to run inside the parentheses +/// - Returns: A parser that parses a parenthesized expression +pub fn parenthesized<'a, Parser, R>(inner: Parser) -> impl FnMut(&'a str) -> IResult<&'a str, R> + where + Parser: Fn(&'a str) -> IResult<&'a str, R> { + delimited( + char('('), + trim(inner), + char(')'), + ) +} + +/// Take where the predicate is true and the length is exactly `n` +/// - Parameters +/// - `n`: The length of the string to take +/// - `predicate`: The predicate to call to validate the input +/// - Returns: A parser that takes `n` characters from the input +pub fn take_where>(n: usize, predicate: F) -> impl Fn(Input) -> IResult + where Input: InputTake + InputIter + InputLength + Slice>, F: Fn(::Item) -> bool, { + move |input: Input| { + take_while_m_n(n, n, |it| predicate(it))(input) + } +} + +pub fn exhausted<'a, Parser, R>(inner: Parser) -> impl FnMut(&'a str) -> IResult<&'a str, R> + where + Parser: Fn(&'a str) -> IResult<&'a str, R> { + terminated(inner, eof) +} + +pub trait IntoResult { + type Error; + fn into_result(self) -> Result; +} + +impl IntoResult for IResult { + type Error = nom::Err>; + fn into_result(self) -> Result { + self.map(|(_remaining, value)| value) + } +} + +#[cfg(test)] +mod tests { + use nom::bytes::streaming::take_while; + use crate::parsing::utils::{exhausted, parenthesized, take_where}; + + #[test] + fn test_parenthesized() { + let input = "(test)"; + let (remaining, result) = parenthesized(take_where(4, |c: char| c.is_ascii_alphabetic()))(input).unwrap(); + assert_eq!(remaining, ""); + assert_eq!(result, "test"); + } + + #[test] + fn test_parenthesized_parse_until_end() { + let input = "(test)"; + assert!(parenthesized(take_while(|_| true))(input).is_err()); + } + + #[test] + fn test_exhausted() { + let input = "test"; + let (remaining, result) = exhausted(take_where(4, |c: char| c.is_ascii_alphabetic()))(input).unwrap(); + assert_eq!(remaining, ""); + assert_eq!(result, "test"); + } + + #[test] + fn test_exhausted_not_exhausted() { + let input = "test "; + assert!(exhausted(take_where(4, |c: char| c.is_ascii_alphabetic()))(input).is_err()); + } +}