bet-1.0.4/.cargo_vcs_info.json0000644000000001360000000000100116100ustar { "git": { "sha1": "d4ee57a5a0b0241e11c0f23b9afc31e526b37505" }, "path_in_vcs": "" }bet-1.0.4/.gitignore000064400000000000000000000000231046102023000123630ustar 00000000000000/target Cargo.lock bet-1.0.4/Cargo.toml0000644000000017740000000000100076170ustar # 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.4" authors = ["dystroy "] build = false autobins = false autoexamples = false autotests = false autobenches = false 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" [lib] name = "bet" path = "src/lib.rs" [dependencies] bet-1.0.4/Cargo.toml.orig000064400000000000000000000006051046102023000132700ustar 00000000000000[package] name = "bet" version = "1.0.4" 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.4/README.md000064400000000000000000000062261046102023000116650ustar 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 sequentially pushing the parts: parenthesis, operators, atoms (the "variables"). You do that by calling the `push_operator`, `open_par`, `close_par` and `push_atom` functions, which build the tree for you. 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. If you wonder whether bet could be applied to your problems, don't hesitate to [come and discuss](https://miaou.dystroy.org/3768). ## Known open-source usages ### dysk **bet** is used in [dysk](https://dystroy.org/dysk) to filter filesystems. Example: `dysk -f '(type=xfs & remote=no) | size > 5T'`. Here, the atoms are `type=xfs`, `remote=no`, and `size > 5T`. To parse such expression, the simplest solution is to first parse it with atoms being simple strings, then apply `try_map_atoms` on the tree to replace the strings with structs which can be efficiently evaluated on many entries. Here's how it's done: ```rust impl FromStr for Filter { type Err = ParseExprError; fn from_str(input: &str) -> Result { // we start by reading the global structure let mut expr: BeTree = 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.mutate_or_create_atom(String::new).push(c), } } // then we parse each leaf let expr = expr.try_map_atoms(|raw| raw.parse())?; Ok(Self { expr }) } } ``` ## broot In [broot](https://dystroy.org/broot), **bet** enables composite queries on files. For example, `!lock&(carg|c/carg/)` looks for files whose name or content contains "carg", but excluding files whose name contains "lock". ## rhit **bet** is used in [rhit](https://dystroy.org/rhit) to filter log lines. For example, with `rhit -p 'y & !( \d{4} | sp | bl )'`, you get stats on hits on paths containing "y" but neither a 4 digits number, "sp", nor "bl". bet-1.0.4/bacon.toml000064400000000000000000000042731046102023000123650ustar 00000000000000# This is a configuration file for the bacon tool # # Bacon repository: https://github.com/Canop/bacon # Complete help on configuration: https://dystroy.org/bacon/config/ # You can also check bacon's own bacon.toml file # as an example: https://github.com/Canop/bacon/blob/main/bacon.toml default_job = "check" [jobs.check] command = ["cargo", "check", "--color", "always"] need_stdout = false [jobs.check-all] command = ["cargo", "check", "--all-targets", "--color", "always"] need_stdout = false # Run clippy on the default target [jobs.clippy] command = [ "cargo", "clippy", "--color", "always", ] need_stdout = false # Run clippy on all targets # To disable some lints, you may change the job this way: # [jobs.clippy-all] # command = [ # "cargo", "clippy", # "--all-targets", # "--color", "always", # "--", # "-A", "clippy::bool_to_int_with_if", # "-A", "clippy::collapsible_if", # "-A", "clippy::derive_partial_eq_without_eq", # ] # need_stdout = false [jobs.clippy-all] command = [ "cargo", "clippy", "--all-targets", "--color", "always", ] need_stdout = false # This job lets you run # - all tests: bacon test # - a specific test: bacon test -- config::test_default_files # - the tests of a package: bacon test -- -- -p config [jobs.test] command = [ "cargo", "test", "--color", "always", "--no-fail-fast", "--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 ] need_stdout = true [jobs.doc] command = ["cargo", "doc", "--color", "always", "--no-deps"] need_stdout = false # If the doc compiles, then it opens in your browser and bacon switches # to the previous job [jobs.doc-open] command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] need_stdout = false on_success = "back" # so that we don't open the browser at each change # You may define here keybindings that would be specific to # a project, for example a shortcut to launch a specific job. # Shortcuts to internal functions (scrolling, toggling, etc.) # should go in your personal global prefs.toml file instead. [keybindings] # alt-m = "job:my-job" c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target bet-1.0.4/src/be_tree.rs000064400000000000000000000424001046102023000131420ustar 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 { 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 openness: 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, openness: 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.tail == other.tail && self.last_pushed == other.last_pushed && self.op_count == other.op_count && self.openness == other.openness } } 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(&self) -> std::slice::Iter<'_, 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(&self) -> Option<&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_openness(&self) -> usize { self.openness } 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].right = child; } else { self.nodes[self.tail].left = 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_none() { self.nodes[self.tail].left = Child::Atom(atom_idx); return; } let mut current = self.tail; loop { if self.nodes[current].right.is_none() { self.nodes[current].right = Child::Atom(atom_idx); return; } let Some(parent) = self.nodes[current].parent else { // no parent for {current}, atom is useless return; }; current = parent; } } /// 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.openness += 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.openness -= 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() { self.nodes[self.tail].operator = Some(operator); return; } // 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 let Some(parent_idx) = self.nodes[new_idx].parent else { // the replaced node was the head self.head = new_idx; self.tail = new_idx; return; }; 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); } // we connect the tail to the new node self.nodes[self.tail].parent = Some(new_idx); // and we update the tail self.tail = new_idx; } /// 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::*; matches!(self.last_pushed, Nothing | Operator | OpeningPar) } /// 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 { matches!(self.last_pushed, TokenType::Atom | TokenType::ClosingPar) } /// 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::*; matches!(self.last_pushed, Nothing | Operator | OpeningPar) } /// 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::*; matches!(self.last_pushed, Nothing | Operator | OpeningPar) } /// 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::*; matches!(self.last_pushed, Atom | ClosingPar if self.openness > 0) } /// 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, openness: self.openness, }) } 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); let Some(op) = &node.operator else { return left_value; }; let Some(left_value) = left_value else { // probably pathological return None; }; if short_circuit(op, &left_value) { return Some(left_value); } let right_value = self.eval_child(eval_atom, eval_op, short_circuit, node.right); Some(eval_op(op, left_value, right_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)?; let Some(op) = &node.operator else { return Ok(left_value); }; let Some(left_value) = left_value else { // probably pathological return Ok(None); }; if short_circuit(op, &left_value) { return Ok(Some(left_value)); }; let right_value = self.eval_child_faillible(eval_atom, eval_op, short_circuit, node.right)?; Ok(Some(eval_op(op, left_value, right_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.4/src/child.rs000064400000000000000000000006451046102023000126250ustar 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_none(self) -> bool { matches!(self, Self::None) } pub fn is_some(self) -> bool { !self.is_none() } } bet-1.0.4/src/lib.rs000064400000000000000000000132171046102023000123070ustar 00000000000000/*! 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 sequentially pushing the parts: parenthesis, operators, atoms (the "variables"). You do that by calling the `push_operator`, `open_par`, `close_par` and `push_atom` functions, which build the tree for you. 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. # Examples: Known open-source usages ### dysk **bet** is used in [dysk](https://dystroy.org/dysk) to filter filesystems. Example: `dysk -f '(type=xfs & remote=no) | size > 5T'`. Here, the atoms are `type=xfs`, `remote=no`, and `size > 5T`. To parse such expression, the simplest solution is to first parse it with atoms being simple strings, then apply `try_map_atoms` on the tree to replace the strings with structs which can be efficiently evaluated on many entries. Here's how it's done: ```ignore impl FromStr for Filter { type Err = ParseExprError; fn from_str(input: &str) -> Result { // we start by reading the global structure let mut expr: BeTree = 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.mutate_or_create_atom(String::new).push(c), } } // then we parse each leaf let expr = expr.try_map_atoms(|raw| raw.parse())?; Ok(Self { expr }) } } ``` ## broot In [broot](https://dystroy.org/broot), **bet** enables composite queries on files. For example, `!lock&(carg|c/carg/)` looks for files whose name or content contains "carg", but excluding files whose name contains "lock". ## rhit **bet** is used in [rhit](https://dystroy.org/rhit) to filter log lines. For example, with `rhit -p 'y & !( \d{4} | sp | bl )'`, you get stats on hits on paths containing "y" but neither a 4 digits number, "sp", nor "bl". # Complete 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 be_tree; mod child; mod node; #[cfg(test)] mod test_bool; #[cfg(test)] mod test_bool_faillible; pub use {be_tree::*, child::*, node::*}; bet-1.0.4/src/node.rs000064400000000000000000000016661046102023000124730ustar 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.4/src/test_bool.rs000064400000000000000000000044561046102023000135400ustar 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 { matches!((self, a), (Self::And, false) | (Self::Or, true)) } } 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); } #[test] fn issue_2() { check("F | F | F", false); check("F | F | F | F", false); check("F | T | F", true); check("F | T | F | F", true); check("F | F & F", false); check("F | F & F | F", false); check("F | T & F", false); check("F | T & F | F", false); check("F | F | T & F", false); } bet-1.0.4/src/test_bool_faillible.rs000064400000000000000000000041601046102023000155330ustar 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"), } } /// tell whether we can skip evaluating the second operand fn short_circuit(self, a: bool) -> bool { matches!((self, a), (Self::And, false) | (Self::Or, true)) } } 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| op.short_circuit(*a), ); 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 }