boolean_expression/expr.rs
1// boolean_expression: a Rust crate for Boolean expressions and BDDs.
2//
3// Copyright (c) 2016 Chris Fallin <cfallin@c1f.net>. Released under the MIT
4// License.
5//
6
7use std::cmp::Ord;
8use std::collections::HashMap;
9use std::fmt::Debug;
10use std::hash::Hash;
11use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not};
12
13use simplify;
14
15/// An `Expr` is a simple Boolean logic expression. It may contain terminals
16/// (i.e., free variables), constants, and the following fundamental operations:
17/// AND, OR, NOT.
18///
19/// ```
20/// use std::collections::HashMap;
21/// use boolean_expression::Expr;
22///
23/// #[derive(Clone, Hash, Eq, PartialEq, Debug)]
24/// enum RiverCrossing { Chicken, Fox, Grain }
25///
26/// let chicken = Expr::Terminal(RiverCrossing::Chicken);
27/// let fox_and_grain = Expr::Terminal(RiverCrossing::Fox) & Expr::Terminal(RiverCrossing::Grain);
28///
29/// let allowed = (!chicken.clone() & fox_and_grain.clone()) | (chicken & !fox_and_grain);
30/// let items = [
31/// (RiverCrossing::Grain, true),
32/// (RiverCrossing::Fox, true),
33/// ].iter().cloned().collect();
34///
35/// // nobody gets eaten!
36/// assert!(allowed.evaluate(&items));
37/// ```
38#[derive(Clone, Debug, PartialEq, Eq, Hash)]
39pub enum Expr<T>
40where
41 T: Clone + Debug + Eq + Hash,
42{
43 /// A terminal (free variable). This expression node represents a value that
44 /// is not known until evaluation time.
45 Terminal(T),
46 /// A boolean constant: true or false.
47 Const(bool),
48
49 /// The logical complement of the contained expression argument.
50 Not(Box<Expr<T>>),
51
52 /// The logical AND of the two expression arguments.
53 And(Box<Expr<T>>, Box<Expr<T>>),
54
55 /// The logical OR of the two expression arguments.
56 Or(Box<Expr<T>>, Box<Expr<T>>),
57}
58
59impl<T> Expr<T>
60where
61 T: Clone + Debug + Eq + Hash,
62{
63 /// Returns `true` if this `Expr` is a terminal.
64 pub fn is_terminal(&self) -> bool {
65 match self {
66 &Expr::Terminal(_) => true,
67 _ => false,
68 }
69 }
70
71 /// Returns `true` if this `Expr` is a constant.
72 pub fn is_const(&self) -> bool {
73 match self {
74 &Expr::Const(_) => true,
75 _ => false,
76 }
77 }
78
79 /// Returns `true` if this `Expr` is a NOT node.
80 pub fn is_not(&self) -> bool {
81 match self {
82 &Expr::Not(_) => true,
83 _ => false,
84 }
85 }
86
87 /// Returns `true` if this `Expr` is an AND node.
88 pub fn is_and(&self) -> bool {
89 match self {
90 &Expr::And(_, _) => true,
91 _ => false,
92 }
93 }
94
95 /// Returns `true` if this `Expr` is an OR node.
96 pub fn is_or(&self) -> bool {
97 match self {
98 &Expr::Or(_, _) => true,
99 _ => false,
100 }
101 }
102
103 /// Builds a NOT node around an argument, consuming the argument
104 /// expression.
105 pub fn not(e: Expr<T>) -> Expr<T> {
106 Expr::Not(Box::new(e))
107 }
108
109 /// Builds an AND node around two arguments, consuming the argument
110 /// expressions.
111 pub fn and(e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
112 Expr::And(Box::new(e1), Box::new(e2))
113 }
114
115 /// Builds an OR node around two arguments, consuming the argument
116 /// expressions.
117 pub fn or(e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
118 Expr::Or(Box::new(e1), Box::new(e2))
119 }
120
121 pub fn xor(e1: Expr<T>, e2: Expr<T>) -> Expr<T> {
122 let nand = !(e1.clone() & e2.clone());
123 let or = e1 | e2;
124 nand & or
125 }
126
127 /// Evaluates the expression with a particular set of terminal assignments.
128 /// If any terminals are not assigned, they default to `false`.
129 pub fn evaluate(&self, vals: &HashMap<T, bool>) -> bool {
130 match self {
131 &Expr::Terminal(ref t) => *vals.get(t).unwrap_or(&false),
132 &Expr::Const(val) => val,
133 &Expr::And(ref a, ref b) => a.evaluate(vals) && b.evaluate(vals),
134 &Expr::Or(ref a, ref b) => a.evaluate(vals) || b.evaluate(vals),
135 &Expr::Not(ref x) => !x.evaluate(vals),
136 }
137 }
138
139 /// Evaluates the expression using the provided function to map terminals
140 /// (variables) to boolean values. This is a generalization of
141 /// [`Expr::evaluate`], where the variable lookup in a hashmap is replaced
142 /// with an arbitrary computation.
143 ///
144 ///```
145 /// use boolean_expression::Expr;
146 ///
147 /// let expression = Expr::Terminal(10) | Expr::Terminal(3);
148 ///
149 /// // check if the expression satisfies a predicate
150 /// assert!(expression.evaluate_with(|&x| x > 5));
151 /// ```
152 pub fn evaluate_with<F>(&self, f: F) -> bool
153 where
154 F: Fn(&T) -> bool,
155 {
156 self.evaluate_with1(&f)
157 }
158
159 fn evaluate_with1<F>(&self, f: &F) -> bool
160 where
161 F: Fn(&T) -> bool,
162 {
163 match self {
164 Expr::Terminal(t) => f(t),
165 Expr::Const(val) => *val,
166 Expr::And(a, b) => a.evaluate_with1(f) && b.evaluate_with1(f),
167 Expr::Or(a, b) => a.evaluate_with1(f) || b.evaluate_with1(f),
168 Expr::Not(x) => !x.evaluate_with1(f),
169 }
170 }
171
172 /// Simplify an expression in a relatively cheap way using well-known logic
173 /// identities.
174 ///
175 /// This function performs certain reductions using DeMorgan's Law,
176 /// distribution of ANDs over ORs, and certain identities involving
177 /// constants, to simplify an expression. The result will always be in
178 /// sum-of-products form: nodes will always appear in order (from
179 /// expression tree root to leaves) `OR -> AND -> NOT`. In other words,
180 /// `AND` nodes may only have `NOT` nodes (or terminals or constants) as
181 /// children, and `NOT` nodes may only have terminals or constants as
182 /// children.
183 ///
184 /// This function explicitly does *not* perform any sort of minterm reduction.
185 /// The terms may thus be redundant (i.e., `And(a, b)` may appear twice), and
186 /// combinable terms may exist (i.e., `And(a, b)` and `And(a, Not(b))` may
187 /// appear in the `OR`'d list of terms, where these could be combined to
188 /// simply `a` but are not). For example:
189 ///
190 /// ```
191 /// use boolean_expression::Expr;
192 ///
193 /// // This simplifies using DeMorgan's Law:
194 /// let expr = Expr::not(Expr::or(Expr::Terminal(0), Expr::Terminal(1)));
195 /// let simplified = expr.simplify_via_laws();
196 /// assert_eq!(simplified,
197 /// Expr::and(Expr::not(Expr::Terminal(0)),
198 /// Expr::not(Expr::Terminal(1))));
199 ///
200 /// // This doesn't simplify, though:
201 /// let expr = Expr::or(
202 /// Expr::and(Expr::Terminal(0), Expr::Terminal(1)),
203 /// Expr::and(Expr::Terminal(0), Expr::not(Expr::Terminal(1))));
204 /// let simplified = expr.clone().simplify_via_laws();
205 /// assert_eq!(simplified, expr);
206 /// ```
207 ///
208 /// For better (but more expensive) simplification, see `simplify_via_bdd()`.
209 pub fn simplify_via_laws(self) -> Expr<T> {
210 simplify::simplify_via_laws(self)
211 }
212
213 /// Simplify an expression via a roundtrip through a `BDD`. This procedure
214 /// is more effective than `Expr::simplify_via_laws()`, but more expensive.
215 /// This roundtrip will implicitly simplify an arbitrarily complicated
216 /// function (by construction, as the BDD is built), and then find a
217 /// quasi-minimal set of terms using cubelist-based reduction. For example:
218 ///
219 /// ```
220 /// use boolean_expression::Expr;
221 ///
222 /// // `simplify_via_laws()` cannot combine these terms, but
223 /// // `simplify_via_bdd()` will:
224 /// let expr = Expr::or(
225 /// Expr::and(Expr::Terminal(0), Expr::Terminal(1)),
226 /// Expr::and(Expr::Terminal(0), Expr::not(Expr::Terminal(1))));
227 /// let simplified = expr.simplify_via_bdd();
228 /// assert_eq!(simplified, Expr::Terminal(0));
229 /// ```
230 pub fn simplify_via_bdd(self) -> Expr<T> {
231 simplify::simplify_via_bdd(self)
232 }
233
234 /// Map terminal values using the specified mapping function.
235 pub fn map<F, R>(&self, f: F) -> Expr<R>
236 where
237 F: Fn(&T) -> R,
238 R: Clone + Debug + Eq + Hash,
239 {
240 self.map1(&f)
241 }
242
243 fn map1<F, R>(&self, f: &F) -> Expr<R>
244 where
245 F: Fn(&T) -> R,
246 R: Clone + Debug + Eq + Hash,
247 {
248 match self {
249 &Expr::Terminal(ref t) => Expr::Terminal(f(t)),
250 &Expr::Const(val) => Expr::Const(val),
251 &Expr::Not(ref n) => Expr::not(n.map1(f)),
252 &Expr::And(ref a, ref b) => Expr::and(a.map1(f), b.map1(f)),
253 &Expr::Or(ref a, ref b) => Expr::or(a.map1(f), b.map1(f)),
254 }
255 }
256}
257
258impl<T> Not for Expr<T>
259where
260 T: Clone + Debug + Eq + Hash,
261{
262 type Output = Self;
263
264 fn not(self) -> Self::Output {
265 Self::not(self)
266 }
267}
268
269impl<T> BitAnd<Expr<T>> for Expr<T>
270where
271 T: Clone + Debug + Eq + Hash,
272{
273 type Output = Self;
274
275 fn bitand(self, rhs: Expr<T>) -> Self::Output {
276 Self::and(self, rhs)
277 }
278}
279
280impl<T> BitAndAssign<Expr<T>> for Expr<T>
281where
282 T: Clone + Debug + Eq + Hash,
283{
284 fn bitand_assign(&mut self, rhs: Expr<T>) {
285 *self = Self::and(self.clone(), rhs);
286 }
287}
288
289impl<T> BitOr<Expr<T>> for Expr<T>
290where
291 T: Clone + Debug + Eq + Hash,
292{
293 type Output = Self;
294
295 fn bitor(self, rhs: Expr<T>) -> Self::Output {
296 Self::or(self, rhs)
297 }
298}
299
300impl<T> BitOrAssign<Expr<T>> for Expr<T>
301where
302 T: Clone + Debug + Eq + Hash,
303{
304 fn bitor_assign(&mut self, rhs: Expr<T>) {
305 *self = Self::or(self.clone(), rhs);
306 }
307}
308
309impl<T> BitXor<Expr<T>> for Expr<T>
310where
311 T: Clone + Debug + Eq + Hash,
312{
313 type Output = Self;
314
315 fn bitxor(self, rhs: Expr<T>) -> Self::Output {
316 Self::xor(self, rhs)
317 }
318}
319
320impl<T> BitXorAssign<Expr<T>> for Expr<T>
321where
322 T: Clone + Debug + Eq + Hash,
323{
324 fn bitxor_assign(&mut self, rhs: Expr<T>) {
325 *self = Self::xor(self.clone(), rhs);
326 }
327}