bet-1.0.2/.cargo_vcs_info.json0000644000000001360000000000100116060ustar { "git": { "sha1": "c4d3255fe9c061b3f74bf55c93bf93ab16a5da6c" }, "path_in_vcs": "" }bet-1.0.2/.gitignore000064400000000000000000000000231046102023000123610ustar 00000000000000/target Cargo.lock bet-1.0.2/Cargo.toml0000644000000015720000000000100076110ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "bet" version = "1.0.2" authors = ["dystroy "] description = "Helps parsing and evaluating binary expression trees" readme = "README.md" keywords = [ "binary", "expression", "tree", "parser", ] categories = [ "data-structures", "parsing", "template-engine", ] license = "MIT" repository = "https://github.com/Canop/bet" [dependencies] bet-1.0.2/Cargo.toml.orig000064400000000000000000000006051046102023000132660ustar 00000000000000[package] name = "bet" version = "1.0.2" authors = ["dystroy "] edition = "2018" keywords = ["binary", "expression", "tree", "parser"] license = "MIT" categories = ["data-structures", "parsing", "template-engine"] description = "Helps parsing and evaluating binary expression trees" repository = "https://github.com/Canop/bet" readme = "README.md" [dependencies] bet-1.0.2/README.md000064400000000000000000000033171046102023000116610ustar 00000000000000[![MIT][s2]][l2] [![Latest Version][s1]][l1] [![docs][s3]][l3] [![Chat on Miaou][s4]][l4] [s1]: https://img.shields.io/crates/v/bet.svg [l1]: https://crates.io/crates/bet [s2]: https://img.shields.io/badge/license-MIT-blue.svg [l2]: LICENSE [s3]: https://docs.rs/bet/badge.svg [l3]: https://docs.rs/bet/ [s4]: https://miaou.dystroy.org/static/shields/room.svg [l4]: https://miaou.dystroy.org/3 A library building and preparing expressions, for example boolean expressions such as `(A | B) & !(C | D | E)`, which can be executed on dynamic contents. An expression is built by calling the `push_operator`, `open_par`, `close_par` and `push_atom` functions. It can then be evaluated with the `eval` function which takes as parameters * a function which gives a value to an atom * a function which, given an operator and one or two values, gives a new value * a function deciding whether to short-circuit Normal evaluation order is left to right but is modified with parenthesis. **bet** is designed around separation of building, transformations, and evaluation, so that an expression can be efficiently applied on many inputs. **bet** is designed for very fast evaluation. **bet** is used in [broot](https://dystroy.org/broot) to let users type composite queries on files. **bet** is used in [rhit](https://dystroy.org/rhit) to filter log lines. **bet** is used in [lfs](https://dystroy.org/lfs) to filter filesystems. **Usage and documentation: [docs.rs/bet](https://docs.rs/bet/)** If you wonder whether bet could be applied to your problems, don't hesitate to [come and discuss](https://miaou.dystroy.org/3768). If you know a documented crate with overlapping use cases, tell me too so that I may list it here as alternative. bet-1.0.2/src/be_tree.rs000064400000000000000000000434611046102023000131500ustar 00000000000000use { crate::*, std::fmt, }; pub type AtomId = usize; #[derive(Debug, Clone, Copy, PartialEq)] enum TokenType { Nothing, Atom, Operator, OpeningPar, ClosingPar, } /// Something that can be added to the tree pub enum Token where Op: fmt::Debug + Clone + PartialEq, Atom: fmt::Debug + Clone, { Atom(Atom), Operator(Op), OpeningParenthesis, ClosingParenthesis, } /// An expression which may contain unary and binary operations #[derive(Debug, Clone)] pub struct BeTree where Op: fmt::Debug + Clone + PartialEq, Atom: fmt::Debug + Clone, { atoms: Vec, nodes: Vec>, head: NodeId, // node index - where to start iterating tail: NodeId, // node index - where to add new nodes last_pushed: TokenType, op_count: usize, // number of operators openess: usize, // opening pars minus closing pars } impl Default for BeTree where Op: fmt::Debug + Clone + PartialEq, Atom: fmt::Debug + Clone, { fn default() -> Self { Self { atoms: Vec::new(), nodes: vec![Node::empty()], head: 0, tail: 0, last_pushed: TokenType::Nothing, op_count: 0, openess: 0, } } } impl PartialEq for BeTree where Op: fmt::Debug + Clone + PartialEq, Atom: fmt::Debug + Clone + PartialEq, { fn eq(&self, other: &Self) -> bool { self.atoms == other.atoms && self.nodes == other.nodes && self.head == other.head && self.head == other.head && self.tail == other.tail && self.last_pushed == other.last_pushed && self.op_count == other.op_count && self.openess == other.openess } } impl BeTree where Op: fmt::Debug + Clone + PartialEq, Atom: fmt::Debug + Clone, { /// create an empty expression, ready to be completed pub fn new() -> Self { Self::default() } pub fn node(&self, node_id: NodeId) -> Option<&Node> { self.nodes.get(node_id) } pub fn atom(&self, atom_id: AtomId) -> Option<&Atom> { self.atoms.get(atom_id) } pub fn head(&self) -> &Node { &self.nodes[self.head] } //#[inline(always)] //fn node_unchecked(&self, node_id: NodeId) -> &Node { // self.nodes[node_id.0] //} //#[inline(always)] //fn node_unchecked_mut(&mut self, node_id: NodeId) -> &mut Node { // self.nodes[node_id.0] //} /// tells whether the expression is devoid of any atom pub fn is_empty(&self) -> bool { self.atoms.is_empty() } /// tell whether the tree is exactly one atom pub fn is_atomic(&self) -> bool { self.atoms.len() == 1 && self.op_count == 0 } /// take the atoms of the tree pub fn atoms(self) -> Vec { self.atoms } /// iterate on all atoms pub fn iter_atoms<'a>(&'a self) -> std::slice::Iter<'a, Atom> { self.atoms.iter() } /// returns a reference to the last atom if it's the last /// pushed token. Return none in other cases (including /// when no atom has been pushed at all) pub fn current_atom<'a>(&'a self) -> Option<&'a Atom> { if self.last_pushed == TokenType::Atom { self.atoms.last() } else { None } } /// return the count of open parenthesis minus the /// one of closing parenthesis. Illegal closing parenthesis /// are ignored (hence why this count can be a usize) pub fn get_openess(&self) -> usize { self.openess } fn store_node(&mut self, node: Node) -> usize { self.nodes.push(node); self.nodes.len() - 1 } fn store_atom(&mut self, atom: Atom) -> usize { self.atoms.push(atom); self.atoms.len() - 1 } fn add_child(&mut self, child: Child) { debug_assert!(!self.nodes[self.tail].is_full()); if !self.nodes[self.tail].left.is_some() { self.nodes[self.tail].left = child; } else { self.nodes[self.tail].right = child; } } fn add_child_node(&mut self, child_idx: usize) { self.nodes[child_idx].parent = Some(self.tail); self.add_child(Child::Node(child_idx)); self.tail = child_idx; } /// add one of the possible token: parenthesis, operator or atom pub fn push(&mut self, token: Token) { match token { Token::Atom(atom) => self.push_atom(atom), Token::Operator(op) => self.push_operator(op), Token::OpeningParenthesis => self.open_par(), Token::ClosingParenthesis => self.close_par(), } } /// add an atom in a left-to-right expression building pub fn push_atom(&mut self, atom: Atom) { self.last_pushed = TokenType::Atom; let atom_idx = self.store_atom(atom); if !self.nodes[self.tail].left.is_some() { self.nodes[self.tail].left = Child::Atom(atom_idx); } else { self.nodes[self.tail].right = Child::Atom(atom_idx); } } /// if the last change was an atom pushed or modified, return a mutable /// reference to this atom. If not, push a new atom and return a mutable /// reference to it. pub fn mutate_or_create_atom(&mut self, create: Create) -> &mut Atom where Create: Fn() -> Atom, { if self.last_pushed != TokenType::Atom { self.push_atom(create()); } self.atoms.last_mut().unwrap() } /// add an opening parenthesis to the expression pub fn open_par(&mut self) { self.last_pushed = TokenType::OpeningPar; let node_idx = self.store_node(Node::empty()); self.add_child_node(node_idx); self.openess += 1; } /// add a closing parenthesis to the expression pub fn close_par(&mut self) { self.last_pushed = TokenType::ClosingPar; if let Some(parent) = self.nodes[self.tail].parent { self.tail = parent; self.openess -= 1; } // we might want to return an error if there are too // many closing parenthesis in the future } fn push_unary_operator(&mut self, operator: Op) { let node_idx = self.store_node(Node { operator: Some(operator), parent: Some(self.tail), left: Child::None, right: Child::None, unary: true, }); self.add_child(Child::Node(node_idx)); self.tail = node_idx; } fn push_binary_operator(&mut self, operator: Op) { if self.nodes[self.tail].is_full() { // we replace the current tail // which becomes the left child of the new node let new_idx = self.store_node(Node { operator: Some(operator), parent: self.nodes[self.tail].parent, left: Child::Node(self.tail), right: Child::None, unary: false, }); // we connect the parent to the new node if let Some(parent_idx) = self.nodes[new_idx].parent { if self.nodes[parent_idx].left == Child::Node(self.tail) { // the connection was to the left child self.nodes[parent_idx].left = Child::Node(new_idx); } else { // it must have been to the right child debug_assert_eq!(self.nodes[parent_idx].right, Child::Node(self.tail)); self.nodes[parent_idx].right = Child::Node(new_idx); } } else { // the replaced node was the head self.head = new_idx; } // we connect the tail to the new node //if let Child::Node(child_idx) = self.nodes[self.tail]I self.nodes[self.tail].parent = Some(new_idx); // and we update the tail self.tail = new_idx; } else { self.nodes[self.tail].operator = Some(operator); } } /// add an operator right of the expression /// /// The context will decide whether it's unary or binary pub fn push_operator(&mut self, operator: Op) { match self.last_pushed { TokenType::Atom | TokenType::ClosingPar => { // the operator is binary self.push_binary_operator(operator); } _ => { // the operator is unary self.push_unary_operator(operator); } } self.last_pushed = TokenType::Operator; self.op_count += 1; } /// tell whether it would make sense to push a unary /// operator at this point (for example it makes no /// sense just after an atom) pub fn accept_unary_operator(&self) -> bool { use TokenType::*; match self.last_pushed { Nothing | Operator | OpeningPar => true, _ => false, } } /// tell whether it would make sense to push a binary /// operator at this point (for example it makes no /// sense just after another operator) pub fn accept_binary_operator(&self) -> bool { use TokenType::*; match self.last_pushed { Atom | ClosingPar => true, _ => false, } } /// tell whether it would make sense to push an atom /// at this point (for example it makes no /// sense just after a closing parenthesis) pub fn accept_atom(&self) -> bool { use TokenType::*; match self.last_pushed { Nothing | Operator | OpeningPar => true, _ => false, } } /// tell whether it would make sense to open a parenthesis /// at this point (for example it makes no sense just after /// a closing parenthesis) pub fn accept_opening_par(&self) -> bool { use TokenType::*; match self.last_pushed { Nothing | Operator | OpeningPar => true, _ => false, } } /// tell whether it would make sense to close a parenthesis /// at this point (for example it makes no sense just after /// an operator or if there are more closing parenthesis than /// opening ones) pub fn accept_closing_par(&self) -> bool { use TokenType::*; match self.last_pushed { Atom | ClosingPar if self.openess > 0 => true, _ => false, } } /// produce a new expression by applying a transformation on all atoms /// /// The operation will stop at the first error #[inline] pub fn try_map_atoms(&self, f: F) -> Result, Err> where Atom2: fmt::Debug + Clone, F: Fn(&Atom) -> Result, { let mut atoms = Vec::new(); for atom in &self.atoms { atoms.push(f(atom)?); } Ok(BeTree { atoms, nodes: self.nodes.clone(), head: self.head, tail: self.tail, last_pushed: self.last_pushed, op_count: self.op_count, openess: self.openess, }) } fn eval_child( &self, eval_atom: &EvalAtom, eval_op: &EvalOp, short_circuit: &ShortCircuit, child: Child, ) -> Option where EvalAtom: Fn(&Atom) -> R, EvalOp: Fn(&Op, R, Option) -> R, ShortCircuit: Fn(&Op, &R) -> bool, { match child { Child::None => None, Child::Node(node_idx) => self.eval_node(eval_atom, eval_op, short_circuit, node_idx), Child::Atom(atom_idx) => Some(eval_atom(&self.atoms[atom_idx])), } } fn eval_child_faillible( &self, eval_atom: &EvalAtom, eval_op: &EvalOp, short_circuit: &ShortCircuit, child: Child, ) -> Result, Err> where EvalAtom: Fn(&Atom) -> Result, EvalOp: Fn(&Op, R, Option) -> Result, ShortCircuit: Fn(&Op, &R) -> bool, { Ok(match child { Child::None => None, Child::Node(node_idx) => self.eval_node_faillible(eval_atom, eval_op, short_circuit, node_idx)?, Child::Atom(atom_idx) => Some(eval_atom(&self.atoms[atom_idx])?), }) } fn eval_node( &self, eval_atom: &EvalAtom, eval_op: &EvalOp, short_circuit: &ShortCircuit, node_idx: usize, ) -> Option where EvalAtom: Fn(&Atom) -> R, EvalOp: Fn(&Op, R, Option) -> R, ShortCircuit: Fn(&Op, &R) -> bool, { let node = &self.nodes[node_idx]; let left_value = self.eval_child(eval_atom, eval_op, short_circuit, node.left); if let Some(op) = &node.operator { if let Some(left_value) = left_value { if short_circuit(op, &left_value) { Some(left_value) } else { let right_value = self.eval_child(eval_atom, eval_op, short_circuit, node.right); Some(eval_op(op, left_value, right_value)) } } else { // probably pathological None } } else { left_value } } fn eval_node_faillible( &self, eval_atom: &EvalAtom, eval_op: &EvalOp, short_circuit: &ShortCircuit, node_idx: usize, ) -> Result, Err> where EvalAtom: Fn(&Atom) -> Result, EvalOp: Fn(&Op, R, Option) -> Result, ShortCircuit: Fn(&Op, &R) -> bool, { let node = &self.nodes[node_idx]; let left_value = self.eval_child_faillible(eval_atom, eval_op, short_circuit, node.left)?; Ok( if let Some(op) = &node.operator { if let Some(left_value) = left_value { if short_circuit(op, &left_value) { Some(left_value) } else { let right_value = self.eval_child_faillible( eval_atom, eval_op, short_circuit, node.right, )?; Some(eval_op(op, left_value, right_value)?) } } else { // probably pathological None } } else { left_value } ) } /// evaluate the expression. /// /// `eval_atom` will be called on all atoms (leafs) of the expression while `eval_op` /// will be used to join values until the final result is obtained. /// /// `short_circuit` will be called on all binary operations with the operator /// and the left operands as arguments. If it returns `true` then the right /// operand isn't evaluated (it's guaranteed so it may serve as guard). /// /// This function should be used when neither atom evaluation nor operator /// execution can raise errors (this usually means consistency checks have /// been done during parsing). #[inline] pub fn eval( &self, eval_atom: EvalAtom, eval_op: EvalOp, short_circuit: ShortCircuit, ) -> Option where EvalAtom: Fn(&Atom) -> R, EvalOp: Fn(&Op, R, Option) -> R, ShortCircuit: Fn(&Op, &R) -> bool, { self.eval_node(&eval_atom, &eval_op, &short_circuit, self.head) } /// evaluate the expression. /// /// `eval_atom` will be called on all atoms (leafs) of the expression while `eval_op` /// will be used to join values until the final result is obtained. /// /// `short_circuit` will be called on all binary operations with the operator /// and the left operands as arguments. If it returns `true` then the right /// operand isn't evaluated (it's guaranteed so it may serve as guard). /// /// This function should be used when errors are expected during either atom /// evaluation or operator execution (for example because parsing was lax). /// The first Error returned by one of those functions breaks the evaluation /// and is returned. #[inline] pub fn eval_faillible( &self, eval_atom: EvalAtom, eval_op: EvalOp, short_circuit: ShortCircuit, ) -> Result, Err> where EvalAtom: Fn(&Atom) -> Result, EvalOp: Fn(&Op, R, Option) -> Result, ShortCircuit: Fn(&Op, &R) -> bool, { self.eval_node_faillible(&eval_atom, &eval_op, &short_circuit, self.head) } pub fn simplify(&mut self) { while let Node { operator: None, left: Child::Node(node_id), parent: None, right: Child::None, unary: false, } = self.nodes[self.head] { self.nodes[node_id].parent = None; self.head = node_id; } } pub fn print_child(&self, child: Child, indent: usize) { for _ in 0..indent { print!(" "); } match child { Child::None => println!("-"), Child::Node(node_id) => self.print_node(node_id, indent + 1), Child::Atom(atom_id) => println!("{:?}", &self.atoms[atom_id]), } } pub fn print_node(&self, node_id: NodeId, indent: usize) { let node = &self.nodes[node_id]; println!("[{}] {:?}", node_id, &node.operator); self.print_child(node.left, indent+1); self.print_child(node.right, indent+1); } pub fn print_tree(&self) { self.print_node(self.head, 0); } } bet-1.0.2/src/child.rs000064400000000000000000000006321046102023000126170ustar 00000000000000use crate::*; /// One of the children of a node /// /// You probably don't need to use this struct unless /// you want to inspect the binary expression tree. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Child { None, Node(NodeId), Atom(AtomId), } impl Child { pub fn is_some(self) -> bool { match self { Self::None => false, _ => true, } } } bet-1.0.2/src/lib.rs000064400000000000000000000070451046102023000123070ustar 00000000000000/*! A simple binary expression tree, for parsing and preparing expressions which can be executed on dynamic contents. An expression is built by calling the `push_operator`, `open_par`, `close_par` and `push_atom` functions. It can then be evaluated with the `eval` function which takes as parameters * a function which gives a value to an atom * a function which, given an operator and one or two values, gives a new value * a function deciding whether to shortcut Normal evaluation order is left to right but is modified with parenthesis. # Example : parsing and evaluating boolean expressions Here we parse expressions like `"(A | B) & !(C | D | E)"` and evaluate them. ``` use bet::BeTree; /// The operators in this example are AND, OR, and NOT operating on booleans. /// `And` and `Or` are binary while `Not` is unary. #[derive(Debug, Clone, Copy, PartialEq)] enum BoolOperator { And, Or, Not, } /// simple but realistic example of an expression parsing. /// /// You don't have to parse tokens in advance, you may accumulate /// into atoms with `mutate_or_create_atom`. /// /// For more reliable results on user inputs, you may want to check /// the consistency (or use default operators) during parsing /// with `accept_atom`, `accept_unary_operator`, etc. fn parse(input: &str) -> BeTree { let mut expr = BeTree::new(); for c in input.chars() { match c { '&' => expr.push_operator(BoolOperator::And), '|' => expr.push_operator(BoolOperator::Or), '!' => expr.push_operator(BoolOperator::Not), ' ' => {}, '(' => expr.open_par(), ')' => expr.close_par(), _ => expr.push_atom(c), } } expr } /// evaluate the expression. /// /// `trues` is the set of chars whose value is true. /// /// If no operation is expected to fail, you may use /// `eval` instead of `eval_faillible`, for a simpler /// API. fn eval(expr: &BeTree, trues: &[char]) -> bool { expr.eval_faillible( // the function evaluating leafs - here it's simple |c| Ok(trues.contains(c)), // the function applying an operator to one or two values |op, a, b| match (op, b) { (BoolOperator::And, Some(b)) => Ok(a & b), (BoolOperator::Or, Some(b)) => Ok(a | b), (BoolOperator::Not, None) => Ok(!a), _ => { Err("unexpected operation") } }, // when to short-circuit. This is essential when leaf // evaluation is expensive or when the left part guards // for correctness of the right part evaluation |op, a| match (op, a) { (BoolOperator::And, false) => true, (BoolOperator::Or, true) => true, _ => false, }, ).unwrap().unwrap() } // checking complex evaluations with T=true and F=false assert_eq!(eval(&parse("!((T|F)&T)"), &['T']), false); assert_eq!(eval(&parse("!(!((T|F)&(F|T)&T)) & !F & (T | (T|F))"), &['T']), true); // we evaluate an expression with two different sets of values let expr = parse("(A | B) & !(C | D | E)"); assert_eq!(eval(&expr, &['A', 'C', 'E']), false); assert_eq!(eval(&expr, &['A', 'B']), true); // Let's show the left to right evaluation order // and importance of parenthesis assert_eq!(eval(&parse("(A & B)|(C & D)"), &['A', 'B', 'C']), true); assert_eq!(eval(&parse(" A & B | C & D "), &['A', 'B', 'C']), false); ``` */ mod child; mod node; mod be_tree; #[cfg(test)] mod test_bool; #[cfg(test)] mod test_bool_faillible; pub use { child::*, node::*, be_tree::*, }; bet-1.0.2/src/node.rs000064400000000000000000000017021046102023000124600ustar 00000000000000use { crate::Child, std::fmt, }; pub type NodeId = usize; /// A node in the expression tree /// /// You probably don't need to use this struct /// unless you want to inspect the tree #[derive(Debug, Clone, PartialEq)] pub struct Node where Op: fmt::Debug + Clone + PartialEq, { pub operator: Option, pub parent: Option, pub left: Child, pub right: Child, pub unary: bool, // true when there's an operator in a unary position } impl Node where Op: fmt::Debug + Clone + PartialEq, { /// a node is full when we can't add other childs pub fn is_full(&self) -> bool { if self.unary { self.left.is_some() } else { self.right.is_some() } } pub fn empty() -> Self { Self { operator: None, parent: None, left: Child::None, right: Child::None, unary: false, } } } bet-1.0.2/src/test_bool.rs000064400000000000000000000040641046102023000135310ustar 00000000000000//! some tests with boolean expressions building and evaluating use super::*; #[derive(Debug, Clone, Copy, PartialEq)] enum BoolOperator { And, Or, Not, } impl BoolOperator { fn eval(self, a: bool, b: Option) -> bool { match (self, b) { (Self::And, Some(b)) => a & b, (Self::Or, Some(b)) => a | b, (Self::Not, None) => !a, _ => unreachable!(), } } /// tell whether we can skip evaluating the second operand fn short_circuit(self, a: bool) -> bool { match (self, a) { (Self::And, false) => true, (Self::Or, true) => true, _ => false, } } } fn check(input: &str, expected: bool) { let mut expr = BeTree::new(); for c in input.chars() { match c { '&' => expr.push_operator(BoolOperator::And), '|' => expr.push_operator(BoolOperator::Or), '!' => expr.push_operator(BoolOperator::Not), ' ' => {}, '(' => expr.open_par(), ')' => expr.close_par(), _ => expr.push_atom(c), } } let result = expr.eval( |&c| c == 'T', |op, a, b| op.eval(a, b), |op, &a| op.short_circuit(a), ); assert_eq!(result, Some(expected)); } #[test] fn test_bool() { check("T", true); check("(((T)))", true); check("F", false); check("!T", false); check("!F", true); check("!!F", false); check("!!!F", true); check("F | T", true); check("F & T", false); check("F | !T", false); check("!F | !T", true); check("!(F & T)", true); check("!(T | T)", false); check("T | !(T | T)", true); check("T & (T & F)", false); check("!F & !(T & F & T)", true); check("!((T|F)&T)", false); check("!(!((T|F)&(F|T)&T)) & !F & (T | (T|F))", true); check("(T | F) & !T", false); check("!(T | F | T)", false); check("(T | F) & !(T | F | T)", false); check("F | !T | !(T & T | F)", false); check("(T & T) | (T & F)", true); check("T & T | T & F", false); } bet-1.0.2/src/test_bool_faillible.rs000064400000000000000000000041111046102023000155250ustar 00000000000000//! some tests with boolean expressions building and evaluating use super::*; type BoolErr = &'static str; #[derive(Debug, Clone, Copy, PartialEq)] enum BoolOperator { And, Or, Not, } impl BoolOperator { fn eval(self, a: bool, b: Option) -> Result { match (self, b) { (Self::And, Some(b)) => Ok(a & b), (Self::Or, Some(b)) => Ok(a | b), (Self::Not, None) => Ok(!a), _ => { Err("unexpected operation") } } } } fn check(input: &str, expected: bool) { let mut expr = BeTree::new(); for c in input.chars() { match c { '&' => expr.push_operator(BoolOperator::And), '|' => expr.push_operator(BoolOperator::Or), '!' => expr.push_operator(BoolOperator::Not), ' ' => {}, '(' => expr.open_par(), ')' => expr.close_par(), _ => expr.push_atom(c), } } let result = expr.eval_faillible( |&c| Ok(c=='T'), |op, a, b| op.eval(a, b), |op, a| match (op, a) { // short-circuit (BoolOperator::And, false) => true, (BoolOperator::Or, true) => true, _ => false, }, ); assert_eq!(result, Ok(Some(expected))); } #[test] fn test_bool() { check("T", true); check("(((T)))", true); check("F", false); check("!T", false); check("!F", true); check("!!F", false); check("!!!F", true); check("F | T", true); check("F & T", false); check("F | !T", false); check("!F | !T", true); check("!(F & T)", true); check("!(T | T)", false); check("T | !(T | T)", true); check("T & (T & F)", false); check("!F & !(T & F & T)", true); check("!((T|F)&T)", false); check("!(!((T|F)&(F|T)&T)) & !F & (T | (T|F))", true); check("(T | F) & !T", false); check("!(T | F | T)", false); check("(T | F) & !(T | F | T)", false); check("F | !T | !(T & T | F)", false); check("(T & T) | (T & F)", true); check("T & T | T & F", false); // TODO add unit test checking errors }