implies/formula.rs
1use super::symbol::{Symbol, Symbolic};
2use crate::parser::ParseError;
3use cascade::cascade;
4use enum_iterator::*;
5use std::collections::{HashMap, HashSet};
6use std::fmt::Display;
7use std::hash::Hash;
8
9/// Classic tree implementation, but the enum variants
10/// for Binary, Unary and Atom ensure that ill-formed
11/// formulas can never be constructed. Uses [`Box`]
12/// as internal pointer because we modify formulae through
13/// a zipper. Within the formula struct, represents the
14/// untraversed/'unzipped' parts of the formula.
15#[derive(PartialEq, Hash, Eq, PartialOrd, Ord, Clone, Debug)]
16pub enum Tree<B, U, A>
17where
18 B: Symbolic,
19 U: Symbolic,
20 A: Symbolic,
21{
22 Binary {
23 conn: B,
24 left: Box<Tree<B, U, A>>,
25 right: Box<Tree<B, U, A>>,
26 },
27 Unary {
28 conn: U,
29 next: Box<Tree<B, U, A>>,
30 },
31 Atom(A),
32}
33
34impl<B, U, A> Display for Tree<B, U, A>
35where
36 B: Symbolic,
37 U: Symbolic,
38 A: Symbolic,
39{
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 let mut repr = String::new();
42 self.read_inorder(&mut repr);
43 write!(f, "{}", repr)
44 }
45}
46
47impl<B, U, A> std::default::Default for Tree<B, U, A>
48where
49 B: Symbolic,
50 U: Symbolic,
51 A: Symbolic,
52{
53 /// Assuming `A::default()` returns a 0 value, effectively amounts
54 /// to a null value without allowing invalid trees to be constructed.
55 fn default() -> Self {
56 Tree::Atom(A::default())
57 }
58}
59
60/// Only the most basic manipulations (adding unary operators and combining
61/// formulas) are provided; more complex manipulations are provided by the [`Formula`]
62/// which is much more ergonomic for expressing in-place mutation using its
63/// internal [`Zipper`]
64/// [`Zipper`]: Zipper
65/// [`Formula`]: Formula
66impl<B, U, A> Tree<B, U, A>
67where
68 B: Symbolic,
69 U: Symbolic,
70 A: Symbolic,
71{
72 pub fn new(atom: A) -> Self {
73 Tree::Atom(atom)
74 }
75
76 pub fn is_atomic(&self) -> bool {
77 if let Tree::Atom(_) = self {
78 true
79 } else {
80 false
81 }
82 }
83
84 pub fn is_unary(&self) -> bool {
85 if let Tree::Unary { .. } = self {
86 true
87 } else {
88 false
89 }
90 }
91
92 pub fn is_binary(&self) -> bool {
93 if let Tree::Binary { .. } = self {
94 true
95 } else {
96 false
97 }
98 }
99
100 /// Inorder traversal starting at the current zipper.
101 /// Does not mutate the formula at all but the closure passed
102 /// in is still [`std::ops::FnMut`] so that other state
103 /// can be mutated. As usual returns [`Option<()>`] so that
104 /// traversal can be stopped early by returning `None`.
105 pub fn inorder_traverse<F: FnMut(&Self) -> Option<()>>(&self, func: &mut F) -> Option<()> {
106 Some(match &self {
107 Tree::Binary { left, right, .. } => {
108 left.inorder_traverse(func);
109 func(self)?;
110 right.inorder_traverse(func);
111 }
112 Tree::Unary { next, .. } => {
113 func(self)?;
114 next.inorder_traverse(func);
115 }
116 Tree::Atom(_) => func(self)?,
117 })
118 }
119
120 /// Preorder traversal starting at the current context.
121 /// Also takes in a closure that can read the formula.
122 pub fn preorder_traverse<F: FnMut(&Self) -> Option<()>>(&self, func: &mut F) -> Option<()> {
123 func(self)?;
124 Some(match &self {
125 Tree::Binary { left, right, .. } => {
126 left.preorder_traverse(func);
127 right.preorder_traverse(func);
128 }
129 Tree::Unary { next, .. } => {
130 next.preorder_traverse(func);
131 }
132 Tree::Atom(_) => {}
133 })
134 }
135
136 /// Combine two trees with a binary operator, inserting the new tree
137 /// on the right side.
138 pub fn combine(&mut self, bin: B, formula: Self) {
139 let old = std::mem::take(self);
140 *self = Tree::Binary {
141 conn: bin,
142 left: Box::new(old),
143 right: Box::new(formula),
144 }
145 }
146
147 /// Combine with new tree on the left!
148 pub fn left_combine(&mut self, bin: B, formula: Self) {
149 let old = std::mem::take(self);
150 *self = Tree::Binary {
151 conn: bin,
152 left: Box::new(formula),
153 right: Box::new(old),
154 }
155 }
156
157 /// Add a unary operator to the existing formula.
158 pub fn unify(&mut self, un: U) {
159 let old = std::mem::take(self);
160 *self = Tree::Unary {
161 conn: un,
162 next: Box::new(old),
163 }
164 }
165
166 /// Print the customary inorder traversal of a tree formula into an outparameter.
167 pub fn read_inorder(&self, repr: &mut String) {
168 match self {
169 Tree::Binary { conn, left, right } => {
170 if Symbol::from_tree(left.as_ref()) <= Symbol::Binary(*conn) {
171 repr.push_str("(");
172 left.read_inorder(repr);
173 repr.push_str(")");
174 } else {
175 left.read_inorder(repr)
176 };
177 repr.push_str(&conn.to_string());
178 if Symbol::from_tree(right.as_ref()) < Symbol::Binary(*conn) {
179 repr.push_str("(");
180 right.read_inorder(repr);
181 repr.push_str(")");
182 } else {
183 right.read_inorder(repr)
184 }
185 }
186 Tree::Unary { conn, next } => {
187 repr.push_str(&conn.to_string());
188 if Symbol::from_tree(next.as_ref()) < Symbol::Unary(*conn) {
189 repr.push_str("(");
190 next.read_inorder(repr);
191 repr.push_str(")");
192 } else {
193 next.read_inorder(repr)
194 }
195 }
196 Tree::Atom(a) => repr.push_str(&a.to_string()),
197 }
198 }
199}
200
201/// The thread or 'zipper' that actually tracks where you currently
202/// are in a given tree formula. The recursively nested zippers themselves
203/// contain the node values that trace out a partial walk from the head
204/// of the tree toward a leaf node, i.e. an atom. Zippers contain trees themselves
205/// if and only if they make a 'choice' during the walk, e.g. they traverse
206/// one of two binary subtrees, to retain the choice not made.
207#[derive(PartialEq, Hash, Eq, PartialOrd, Ord, Clone, Debug, Default)]
208pub enum Zipper<B, U, A>
209where
210 B: Symbolic,
211 U: Symbolic,
212 A: Symbolic,
213{
214 #[default]
215 Top,
216 Right {
217 bin: B,
218 sub: Tree<B, U, A>,
219 zip: Box<Zipper<B, U, A>>,
220 },
221 Left {
222 bin: B,
223 sub: Tree<B, U, A>,
224 zip: Box<Zipper<B, U, A>>,
225 },
226 Up {
227 un: U,
228 zip: Box<Zipper<B, U, A>>,
229 },
230}
231
232impl<B: Symbolic, U: Symbolic, A: Symbolic> Zipper<B, U, A> {
233 pub fn is_left(&self) -> bool {
234 if let Zipper::Left { .. } = self {
235 true
236 } else {
237 false
238 }
239 }
240
241 pub fn is_right(&self) -> bool {
242 if let Zipper::Right { .. } = self {
243 true
244 } else {
245 false
246 }
247 }
248
249 pub fn is_up(&self) -> bool {
250 if let Zipper::Up { .. } = self {
251 true
252 } else {
253 false
254 }
255 }
256
257 pub fn is_top(&self) -> bool {
258 if let Zipper::Top = self {
259 true
260 } else {
261 false
262 }
263 }
264
265 /// For formula traversal through the zipper when
266 /// the actual zipper state doesn't need to be changed.
267 pub fn peek_up(&self) -> &Self {
268 match self {
269 Zipper::Top => self,
270 Zipper::Right { zip, .. } | Zipper::Left { zip, .. } | Zipper::Up { zip, .. } => {
271 zip.as_ref()
272 }
273 }
274 }
275
276 /// Flip a right zipper to left or vice versa while retaining
277 /// all the same data.
278 pub fn flip(&mut self) {
279 if let Zipper::Right { bin, sub, zip } = self {
280 *self = Zipper::Left {
281 bin: *bin,
282 sub: std::mem::take(sub),
283 zip: std::mem::take(zip),
284 }
285 } else if let Zipper::Left { bin, sub, zip } = self {
286 *self = Zipper::Right {
287 bin: *bin,
288 sub: std::mem::take(sub),
289 zip: std::mem::take(zip),
290 }
291 }
292 }
293}
294
295/// If you're really lazy I guess!
296impl<B, U, A> From<&Formula<B, U, A>> for Tree<B, U, A>
297where
298 B: Symbolic,
299 U: Symbolic,
300 A: Symbolic,
301{
302 fn from(value: &Formula<B, U, A>) -> Self {
303 value.tree.clone()
304 }
305}
306
307/// The primary generic struct for logical formulas that implement both unary and binary operators.
308/// The struct is generic over the type of binary operators `B`, unary operators `U`, and the atoms `A`,
309/// and assumes all three are very cheap (e.g. fieldless enums, integers) and therefore implement Copy.
310/// It's possible this requirement will be relaxed in future versions for the atoms 'A', in case there's
311/// a need for very complex atoms (i.e. arbitrarily large relations).
312///
313/// This is a [zipper](https://www.st.cs.uni-saarland.de/edu/seminare/2005/advanced-fp/docs/huet-zipper.pdf)
314/// implementation of binary/unary trees which represent logical formulae. This means that a `Formula` is not
315/// just a tree, but a tree *and a particular location in that tree*, represented by the `zipper`; please read
316/// the source material for more if you want to understand how this works. At the basic level you may assume that
317/// `.*_unzip`, methods go 'down' the tree while `.*_zip` methods go up. If the formula is fully zipped (i.e. by calling
318/// [`top_zip`]) the tree is in fact the whole formula, with `Zipper::Top` stored as a sentinel zipper.
319///
320/// The implementation therefore always operates/mutates the formula at its current location in the tree, so if you want
321/// to operate on the very 'top' of the formula you must call [`top_zip`] first. The only exception to this is methods that
322/// start with `.top_`, which you may assume call [`top_zip`] before performing mutations.
323///
324/// The mutating methods therefore change different properties about the formula but leave the 'location' of the formula,
325/// i.e. the current `.tree`, untouched or as untouched as possible, so that these mutations can be done repeatedly as part
326/// of, for example, an [`inorder_traverse_mut`]. It can be hard to understand this abstractly so it's best to read the
327/// documentation for concrete transforming methods like [`combine`] or [`distribute_right`] for a clearer view.
328///
329/// This implementation does *not* use the builder pattern, both out of dislike but also because this would be impossible
330/// to PyBind if methods took self or require a duplicate implementation.
331///
332/// For builder-style method chaining use the wonderful [`cascade!`](https://docs.rs/cascade/latest/cascade/) macro.
333/// The Python wrappers do use a chainable pattern (taking and receiving `&mut self`) and can of course be used
334/// from the Rust API, but the Python API is really designed for Pythonic use and this API for Rust.
335///
336/// Finally, the API is designed for safety in the sense that transformations that are applied are only applied
337/// if they're valid at the current location in the formula and don't do anything otherwise! This is an opinionated
338/// design decision because this crate is fundamentally designed for situations that demand lots of rapid transformation
339/// the circumstance when you want to apply a transformation if possible or not do anything, e.g. when converting a formula
340/// to conjunctive normal form. Returning [`Result<T, E>`] in this situation would inappropriately stop such fast transformation
341/// chains and require a lot of branching in code. Nonetheless a trait may be added and implemented for this type which
342/// reimplements the methods to allow one to immediately discern if a formula has changed after a method call because there's
343/// no signal something has either happened or failed to (e.g. as would be signaled by returning a `Result<T, E>` type).
344///
345/// [`Result<T, E>`]: std::result::Result
346/// [`top_zip`]: Self::top_zip
347/// [`combine`]: Self::combine
348/// [`distribute_right`]: Self::distribute_right
349/// [`inorder_traverse_mut`]: Self::inorder_traverse_mut
350#[derive(PartialEq, Hash, Eq, PartialOrd, Ord, Clone, Debug, Default)]
351pub struct Formula<B, U, A>
352where
353 B: Symbolic,
354 U: Symbolic,
355 A: Symbolic,
356{
357 pub tree: Tree<B, U, A>,
358 pub zipper: Zipper<B, U, A>,
359}
360
361/// This first impl block is for initialization and traversal.
362impl<B: Symbolic, U: Symbolic, A: Symbolic> Formula<B, U, A> {
363 /// A new formula that's just an atom.
364 pub fn new(atom: A) -> Self {
365 Formula {
366 tree: Tree::Atom(atom),
367 zipper: Zipper::Top,
368 }
369 }
370
371 /// Traverse the current zipper one step, reconstructing the tree,
372 /// whether you're in a binary tree (i.e. a left or right zipper)
373 /// or a unary one (up zipper); doesn't do anything if you're at `Zipper::Top`.
374 pub fn zip(&mut self) {
375 match &self.zipper {
376 Zipper::Top => {}
377 Zipper::Right { .. } => self.zip_right(),
378 Zipper::Left { .. } => self.zip_left(),
379 Zipper::Up { .. } => self.zip_up(),
380 }
381 }
382
383 /// In the subtree of a unary tree, go UP and eat the unary operator back into the tree. That is,
384 /// ```text
385 /// zipper: (U, Zipper) zipper: (*Zipper)
386 ///
387 /// ^ => ^
388 /// | |
389 ///
390 /// tree tree: U
391 /// |
392 /// old_tree
393 /// ```
394 pub fn zip_up(&mut self) {
395 if let Zipper::Up { un, zip } = &mut self.zipper {
396 self.tree.unify(*un);
397 self.zipper = std::mem::take((*zip).as_mut());
398 }
399 }
400
401 /// In the left subtree of a binary tree, go "right" and eat the binary operator and right subtree!
402 /// You're basically recombining them, like
403 /// ```text
404 /// zipper: (B, RTree, Zipper) zipper: (*Zipper)
405 /// / |
406 /// / => |
407 /// tree: LTree tree: B
408 /// / \
409 /// LTree RTree
410 /// ```
411 pub fn zip_right(&mut self) {
412 if let Zipper::Right { sub, bin, zip } = &mut self.zipper {
413 self.tree.combine(*bin, std::mem::take(sub));
414 self.zipper = std::mem::take((*zip).as_mut());
415 }
416 }
417
418 /// Just like [`zip_right`] except you're in the right subtree of a
419 /// binary tree. The destination state is the exact same.
420 ///
421 /// [`zip_right`]: Self::zip_right
422 pub fn zip_left(&mut self) {
423 if let Zipper::Left { sub, bin, zip } = &mut self.zipper {
424 self.tree.left_combine(*bin, std::mem::take(sub));
425 self.zipper = std::mem::take((*zip).as_mut());
426 }
427 }
428
429 /// The inverse of [`zip_left`]. Decompose a binary tree and
430 /// traverse to the right subtree of a binary formula.
431 ///
432 /// [`zip_left`]: Self::zip_left
433 pub fn unzip_right(&mut self) {
434 if let Tree::Binary { conn, left, right } = &mut self.tree {
435 self.zipper = Zipper::Left {
436 bin: *conn,
437 sub: std::mem::take((*left).as_mut()),
438 zip: Box::new(std::mem::take(&mut self.zipper)),
439 };
440 self.tree = std::mem::take((*right).as_mut());
441 }
442 }
443
444 /// The inverse of [`zip_right`]: decompose a binary tree and travel
445 /// to the left subtree of a binary formula.
446 ///
447 /// [`zip_right`]: Self::zip_right
448 pub fn unzip_left(&mut self) {
449 if let Tree::Binary { conn, left, right } = &mut self.tree {
450 self.zipper = Zipper::Right {
451 bin: *conn,
452 sub: std::mem::take((*right).as_mut()),
453 zip: Box::new(std::mem::take(&mut self.zipper)),
454 };
455 self.tree = std::mem::take((*left).as_mut());
456 }
457 }
458
459 /// Traverse to the formula contained in a unary tree.
460 /// The inverse of [`zip_up`].
461 ///
462 /// [`zip_up`]: Self::zip_up
463 pub fn unzip_down(&mut self) {
464 if let Tree::Unary { conn, next } = &mut self.tree {
465 self.zipper = Zipper::Up {
466 un: *conn,
467 zip: Box::new(std::mem::take(&mut self.zipper)),
468 };
469 self.tree = std::mem::take((*next).as_mut());
470 }
471 }
472
473 /// Unzip the formula, i.e. return to the top node.
474 /// After this, `self.tree` contains the whole formula.
475 pub fn top_zip(&mut self) {
476 while !self.zipper.is_top() {
477 self.zip();
478 }
479 }
480
481 /// Inorder traversal starting at the current context.
482 /// If you want the whole formula simply [`top_zip`] first.
483 /// Takes in a closure which can mutate the formula in
484 /// place somehow. Returns Option<()> so traversal can be
485 /// stopped early if needed; in most cases can be ignored.
486 ///
487 /// [`top_zip`]: Self::top_zip
488 pub fn inorder_traverse_mut<F: FnMut(&mut Self) -> Option<()>>(
489 &mut self,
490 func: &mut F,
491 ) -> Option<()> {
492 match &self.tree {
493 Tree::Binary { .. } => cascade! {
494 self;
495 ..unzip_left();
496 ..inorder_traverse_mut(func);
497 ..zip(); // a general zip to account for any potential transformations
498 ..apply_mut(func)?;
499 ..unzip_right();
500 ..inorder_traverse_mut(func);
501 ..zip() // a general zip to account for any potential transformations
502 },
503 Tree::Unary { .. } => cascade! {
504 self;
505 ..apply_mut(func)?;
506 ..unzip_down();
507 ..inorder_traverse_mut(func);
508 ..zip() // a general zip to account for any potential transformations
509 },
510 Tree::Atom(_) => self.apply_mut(func)?,
511 }
512 Some(())
513 }
514
515 /// Preorder traversal starting at the current context.
516 /// Also takes in a closure that can mutate the formula.
517 /// This is a much more dangerous traversal strategy to call
518 /// with a transformation method like [`rotate_right`] compared
519 /// to [`inorder_traverse_mut`]; the latter will travel to the leaves
520 /// of a formula and then perform transforms going back *up* the zipper,
521 /// whereas this method will apply transformations immediately as it
522 /// visits each node, having potentially weird consequences if there
523 /// are in place mutations going on with formulae.
524 ///
525 /// [`rotate_right`]: Self::rotate_right
526 /// [`inorder_traverse_mut`]: Self::inorder_traverse_mut
527 pub fn preorder_traverse_mut<F: FnMut(&mut Self) -> Option<()>>(
528 &mut self,
529 func: &mut F,
530 ) -> Option<()> {
531 self.apply_mut(func)?;
532 match &self.tree {
533 Tree::Binary { .. } => cascade! {
534 self;
535 ..apply_mut(func)?;
536 ..unzip_left();
537 ..preorder_traverse_mut(func);
538 ..zip(); // a general zip to account for any potential transformations
539 ..unzip_right();
540 ..preorder_traverse_mut(func);
541 ..zip() // a general zip to account for any potential transformations
542 },
543 Tree::Unary { .. } => cascade! {
544 self;
545 ..apply_mut(func)?;
546 ..unzip_down();
547 ..preorder_traverse_mut(func);
548 ..zip() // a general zip to account for any potential transformations
549 },
550 Tree::Atom(_) => self.apply_mut(func)?,
551 }
552 Some(())
553 }
554
555 pub fn inorder_traverse<F: FnMut(&Tree<B, U, A>) -> Option<()>>(
556 &self,
557 func: &mut F,
558 ) -> Option<()> {
559 Some(self.tree.inorder_traverse(func)?)
560 }
561
562 pub fn preorder_traverse<F: FnMut(&Tree<B, U, A>) -> Option<()>>(
563 &self,
564 func: &mut F,
565 ) -> Option<()> {
566 Some(self.tree.preorder_traverse(func)?)
567 }
568
569 /// Purely for the sake of nicer syntax, allows closures to be called method-style
570 /// as part of method chaining.
571 fn apply_mut<F: FnMut(&mut Self) -> Option<()>>(&mut self, func: &mut F) -> Option<()> {
572 Some(func(self)?)
573 }
574
575 /// Purely for the sake of nicer syntax, allows closures to be called method-style
576 /// as part of method chaining.
577 fn apply<F: FnMut(&Self) -> Option<()>>(&self, func: &mut F) -> Option<()> {
578 Some(func(self)?)
579 }
580}
581
582/// This impl is about manipulating and combining formulas.
583impl<B: Symbolic, U: Symbolic, A: Symbolic> Formula<B, U, A> {
584 /// Connects to a new formula WITHOUT
585 /// unzipping, which is why this takes in a tree.
586 /// Whatever the current `.tree` is will become
587 /// the left subtree of a binary tree, where `new_tree`
588 /// is the right subtree and they're connected by `bin`.
589 /// This is a very zipper-style impl which might be counter
590 /// intuitive, and perhaps better illustrated with some
591 /// poor ASCII art:
592 ///
593 /// ```text
594 /// zipper zipper: Zipper::Right(bin, new_tree, old_zipper)
595 ///
596 /// ^ => /
597 /// | /
598 ///
599 /// tree tree
600 /// ```
601 pub fn combine(&mut self, bin: B, new_tree: Tree<B, U, A>) {
602 self.zipper = Zipper::Right {
603 bin,
604 sub: new_tree,
605 zip: Box::new(std::mem::take(&mut self.zipper)),
606 };
607 }
608
609 /// Exactly the same as [`combine`] but the new subtree is inserted as
610 /// a left subtree, so you're now in the right subtree of a binary tree.
611 /// And therefore you end up with a [`Zipper::Left`].
612 ///
613 /// [`combine`]: Self::combine
614 /// [`Zipper::Left`]: Zipper::Left
615 pub fn left_combine(&mut self, bin: B, new_tree: Tree<B, U, A>) {
616 self.zipper = Zipper::Left {
617 bin,
618 sub: new_tree,
619 zip: Box::new(std::mem::take(&mut self.zipper)),
620 };
621 }
622
623 /// Combine two formulas with a binary connective.
624 /// But unzip first.
625 pub fn top_combine(&mut self, bin: B, mut formula: Self) {
626 formula.top_zip();
627 self.top_zip();
628 self.combine(bin, formula.tree);
629 }
630
631 /// [`top_combine`] but on the left side.
632 ///
633 /// [`top_combine`]: Self::top_combine
634 pub fn top_left_combine(&mut self, bin: B, mut formula: Self) {
635 formula.top_zip();
636 self.top_zip();
637 self.left_combine(bin, formula.tree)
638 }
639
640 /// Insert a unary operator in the formula's current position.
641 pub fn unify(&mut self, un: U) {
642 self.zipper = Zipper::Up {
643 un,
644 zip: Box::new(std::mem::take(&mut self.zipper)),
645 };
646 }
647
648 /// Insert a unary operator for the whole formula.
649 pub fn top_unify(&mut self, un: U) {
650 cascade! {
651 self;
652 ..top_zip();
653 ..unify(un)
654 }
655 }
656
657 /// A function which demonstrates some zipper-y fun, if you're currently at the
658 /// right or left subtree of a binary formula, i.e. the current zipper is
659 /// `Zipper::Right{..}` or `Zipper::Left{..}`, swap your position with the other
660 /// subtree (without moving memory). Otherwise, the formula remains the same.
661 pub fn flip(&mut self) {
662 self.zipper.flip()
663 }
664
665 /// If it applies in the current context, 'rotate' a tree formula,
666 /// i.e. change precedence between two binary operators,
667 /// to the left. As an example,
668 /// ```text
669 /// →
670 /// / \\
671 /// A ∧
672 /// / \
673 /// B C
674 ///
675 /// =>
676 ///
677 /// ∧
678 /// // \
679 /// → C
680 /// / \
681 /// A B
682 /// ```
683 /// is an example of a left rotation.
684 /// Rotations are always performed assuming the current zipper holds the
685 /// lower-precedence `B`, i.e. the one higher up in the tree. In the example
686 /// above, the rotation would be performed on a `Formula` where the `zipper`
687 /// is the \\ pictured, holding the → operator. `self` is left in the same
688 /// position after rotation, with // denoting the new active zipper.
689 pub fn rotate_left(&mut self) {
690 if let Formula {
691 tree:
692 Tree::Binary {
693 conn,
694 left: b,
695 right: c,
696 },
697 zipper: Zipper::Left { bin, sub: a, .. },
698 } = self
699 {
700 std::mem::swap(conn, bin);
701 std::mem::swap(b.as_mut(), c.as_mut());
702 std::mem::swap(a, b.as_mut()); // b now holds the c tree, so really swapping A, C
703 }
704 // You now need a right zipper, so flip it!
705 self.flip()
706 }
707
708 /// If it applies in the current context, 'rotate' a tree formula,
709 /// i.e. change precedence between two binary operators,
710 /// to the right. As an example,
711 /// ```text
712 /// ∧
713 /// // \
714 /// → C
715 /// / \
716 /// A B
717 ///
718 /// =>
719 ///
720 /// →
721 /// / \\
722 /// A ∧
723 /// / \
724 /// B C
725 /// ```
726 /// is an example of a right rotation. More detail available in the
727 /// documentation of [`.rotate_left()`].
728 ///
729 /// [`.rotate_left()`]: Self::rotate_left
730 pub fn rotate_right(&mut self) {
731 if let Formula {
732 tree:
733 Tree::Binary {
734 conn,
735 left: a,
736 right: b,
737 },
738 zipper: Zipper::Right { bin, sub: c, .. },
739 } = self
740 {
741 std::mem::swap(conn, bin);
742 std::mem::swap(a.as_mut(), b.as_mut());
743 std::mem::swap(c, b.as_mut()); // b now holds the a tree, so really swapping A, C
744 }
745 // You now need a left zipper, so flip it!
746 self.flip()
747 }
748
749 /// 'Distribute' a binary operator over the right (binary) subtree.
750 /// Often used in, for example, creating the conjunctive normal forms
751 /// of formulae. Easiest to see by example:
752 /// ```text
753 /// ∧
754 /// / \\
755 /// p ∨
756 /// / \
757 /// q r
758 ///
759 /// =>
760 /// ∨
761 /// / \\
762 /// ∧ ∧
763 /// / \ / \
764 /// p q p r
765 /// ```
766 /// The dummy formula corresponding to `p` above gets cloned.
767 /// Just like with the rotation methods, the above method occurs
768 /// starting from the higher-precedence operator (lower in the subtree)
769 /// corresponding to \\ being the active zipper, and \\ in the second
770 /// formula describes the active zipper after distribution.
771 pub fn distribute_right(&mut self) {
772 if !self.zipper.is_left() || !self.tree.is_binary() {
773 return;
774 }
775 cascade! {
776 let curr = self;
777 ..rotate_left();
778 // while you're here, steal another copy of the formula to distribute
779 let (clone, bin) = if let &Tree::Binary {ref left, conn,.. } = &curr.tree {(left.as_ref().clone(), conn)}
780 else {unreachable!()};
781 ..zip_right();
782 ..unzip_right();
783 ..left_combine(bin, clone)
784 }
785 }
786
787 /// Distribute a binary operator over the left subtree (corresponding to a right
788 /// rotation). See [`distribute_right`] for more.
789 ///
790 /// [`distribute_right`]: Self::distribute_right
791 pub fn distribute_left(&mut self) {
792 if !self.zipper.is_right() || !self.tree.is_binary() {
793 return;
794 }
795 cascade! {
796 let curr = self;
797 ..rotate_right();
798 // while you're here, steal another copy of the formula to distribute
799 let (clone, bin) = if let &Tree::Binary {ref right, conn,.. } = &curr.tree {(right.as_ref().clone(), conn)}
800 else {unreachable!()};
801 ..zip_left();
802 ..unzip_left();
803 ..combine(bin, clone)
804 }
805 }
806
807 /// Very similar to the [`.rotate_*`] methods but with unary operators
808 /// swapping precedence with binary ones instead. Because of this the
809 /// `.lower_*` methods don't reverse each other unlike the [`.rotate_*`] methods.
810 /// This method unifies the right subformula.
811 ///
812 /// [`.rotate_*`]: Self::rotate_right
813 pub fn lower_right(&mut self) {
814 if let Formula {
815 tree: Tree::Binary { right, .. },
816 zipper: Zipper::Up { un, zip },
817 } = self
818 {
819 right.as_mut().unify(*un);
820 self.zipper = *std::mem::take(zip);
821 }
822 }
823
824 /// Same as [`.lower_right()`] but unifies the left subformula.
825 ///
826 /// [`.lower_right()`]: Self::lower_right
827 pub fn lower_left(&mut self) {
828 if let Formula {
829 tree: Tree::Binary { left, .. },
830 zipper: Zipper::Up { un, zip },
831 } = self
832 {
833 left.as_mut().unify(*un);
834 self.zipper = *std::mem::take(zip);
835 }
836 }
837
838 /// Distribute a unary operator over a binary operator. Optionally
839 /// pass in a new binary operator to root the formula after distribution.
840 /// Basically like executing [`lower_left`] and [`lower_right`]
841 /// at the same time, and optionally swapping the root of the binary tree.
842 /// If you're unfamiliar with logic, this method exists because in numerous
843 /// languages a binary operator will have a 'dual' operator that it swaps
844 /// with when a unary operator is distributed over the two operands.
845 ///
846 /// [`lower_left`]: Self::lower_left
847 /// [`lower_right`]: Self::lower_right
848 pub fn distribute_down(&mut self, new_bin: Option<B>) {
849 if let Formula {
850 tree: Tree::Binary { conn, left, right },
851 zipper: Zipper::Up { un, zip },
852 } = self
853 {
854 left.unify(*un);
855 right.unify(*un);
856 if let Some(mut b) = new_bin {
857 std::mem::swap(&mut b, conn)
858 }
859 self.zipper = *std::mem::take(zip);
860 }
861 }
862
863 /// Swap two unary operators, optionally changing the one that's been
864 /// swapped up. This exists because oftentimes unary operators have
865 /// a 'dual' operator which they change to when another unary operator
866 /// changes precedence with them.
867 pub fn push_down(&mut self, new_un: Option<U>) {
868 if let Formula {
869 tree: Tree::Unary { conn, .. },
870 zipper: Zipper::Up { un, .. },
871 } = self
872 {
873 if let Some(mut u) = new_un {
874 std::mem::swap(&mut u, conn)
875 }
876 std::mem::swap(conn, un)
877 }
878 }
879
880 /// Instantiate an *atom* in the formula (as usual, starting where you currently are)
881 /// with another tree subformula. If you want to do this over a whole formula,
882 /// just call this inside [`inorder_traverse_mut`].
883 ///
884 /// [`inorder_traverse_mut`]: Self::inorder_traverse_mut
885 pub fn instantiate(&mut self, formulas: &HashMap<A, Tree<B, U, A>>) {
886 if let Tree::Atom(a) = self.tree {
887 if formulas.contains_key(&a) {
888 self.tree = formulas[&a].clone()
889 }
890 }
891 }
892}
893
894/// This block is about interfacing with tensors.
895impl<B: Symbolic, U: Symbolic, A: Symbolic> Formula<B, U, A> {
896 /// Given some arbitrary mapping from symbols to nonnegative integers, encode
897 /// a formula as a list of integers corresponding to an inorder traversal of
898 /// the nodes, and another list of the parent-child relationships between
899 /// the nodes. If the mapping given is not total, return None.
900 /// The edges are returned in COO format (two lists of pairs,
901 /// corresponding by index).
902 pub fn tensorize(
903 &self,
904 mapping: &HashMap<Symbol<B, U, A>, usize>,
905 ) -> Result<(Vec<usize>, Vec<Vec<usize>>), ParseError> {
906 let mut nodes: Vec<usize> = vec![];
907 let mut edges: Vec<Vec<usize>> = vec![vec![], vec![]];
908
909 let mut counter: usize = 0;
910 let mut stack: Vec<usize> = vec![];
911
912 self.preorder_traverse(&mut |node| {
913 nodes.push(mapping[&Symbol::from_tree(&node)]);
914 if let Some(idx) = stack.pop() {
915 edges[0].push(idx);
916 edges[1].push(counter);
917 }
918 if let Tree::Binary { .. } | Tree::Unary { .. } = node {
919 stack.push(counter)
920 }
921 if let Tree::Binary { .. } = node {
922 stack.push(counter)
923 }
924 Some(counter += 1)
925 })
926 .ok_or(ParseError::IncompleteEnum)?;
927
928 Ok((nodes, edges))
929 }
930
931 /// Store all the atoms that appear in the formula in a HashSet.
932 pub fn get_atoms(&self) -> HashSet<A> {
933 let mut atoms: HashSet<A> = HashSet::new();
934 self.tree.inorder_traverse(&mut |t: &Tree<B, U, A>| {
935 Some(if let &Tree::Atom(a) = t {
936 atoms.insert(a);
937 })
938 });
939 atoms
940 }
941
942 /// Normalize all the atoms in a formula by replacing its atoms with atoms
943 /// that index their first appearance in an inorder traversal.
944 /// The indices given are taken from a passed in iterator (over A) on which
945 /// [`std::iter::Iterator::next`] is called precisely once every time a
946 /// new atom is observed. In case the iterator runs out, returns None.
947 pub fn normalize<I: Iterator<Item = A>>(&self, mut indices: I) -> Option<Self> {
948 let mut seen: HashMap<A, A> = HashMap::new();
949 let mut formula = self.clone();
950 formula.inorder_traverse_mut(&mut |node| {
951 Some(if let Tree::Atom(a) = node.tree {
952 let replacement: &mut A = seen.entry(a).or_insert(indices.next()?);
953 node.tree = Tree::Atom(*replacement);
954 })
955 })?;
956 Some(formula)
957 }
958}
959
960// Special functions for enumerable operator types. Utilities for a rainy day.
961impl<B, U, A> Formula<B, U, A>
962where
963 B: Symbolic + Sequence,
964 U: Symbolic + Sequence,
965 A: Symbolic,
966{
967 /// Give the number of binary operators in this formula type.
968 fn num_binary() -> usize {
969 enum_iterator::cardinality::<B>()
970 }
971 /// Give the number of unary operators in this formula type.
972 fn num_unary() -> usize {
973 enum_iterator::cardinality::<U>()
974 }
975 /// Offer an indexing map for all unary operators.
976 fn unary_counting(offset: usize) -> HashMap<U, usize> {
977 let mut counter = offset..;
978 all::<U>()
979 .map(|s| (s, counter.next().unwrap()))
980 .collect::<HashMap<U, usize>>()
981 }
982
983 /// Offer an indexing map for the string repr of the unary operators.
984 fn unary_str_counting(offset: usize) -> HashMap<String, usize> {
985 let mut counter = offset..;
986 all::<U>()
987 .map(|s| (s.to_string(), counter.next().unwrap()))
988 .collect::<HashMap<String, usize>>()
989 }
990
991 /// Offer an indexing map for all binary operators.
992 fn binary_counting(offset: usize) -> HashMap<B, usize> {
993 let mut counter = offset..;
994 all::<B>()
995 .map(|s| (s, counter.next().unwrap()))
996 .collect::<HashMap<B, usize>>()
997 }
998
999 /// Offer an indexing map for the string repr of the binary operators.
1000 fn binary_str_counting(offset: usize) -> HashMap<String, usize> {
1001 let mut counter = offset..;
1002 all::<B>()
1003 .map(|s| (s.to_string(), counter.next().unwrap()))
1004 .collect::<HashMap<String, usize>>()
1005 }
1006
1007 /// Index all operators for the type, unary first, wrapped in the Symbol type.
1008 fn operator_counting(offset: usize) -> HashMap<Symbol<B, U, A>, usize> {
1009 Self::unary_counting(offset)
1010 .into_iter()
1011 .map(|(u, i)| (Symbol::Unary(u), i))
1012 .chain(
1013 Self::binary_counting(offset + cardinality::<U>())
1014 .into_iter()
1015 .map(|(b, i)| (Symbol::Binary(b), i)),
1016 )
1017 .collect::<HashMap<_, _>>()
1018 }
1019
1020 /// Index all operators for the type, unary first, wrapped in the Symbol type.
1021 fn operator_str_counting(offset: usize) -> HashMap<String, usize> {
1022 Self::unary_str_counting(offset)
1023 .into_iter()
1024 .chain(Self::binary_str_counting(offset + cardinality::<U>()).into_iter())
1025 .collect::<HashMap<_, _>>()
1026 }
1027}
1028
1029impl<B, U, A> Display for Formula<B, U, A>
1030where
1031 B: Symbolic,
1032 U: Symbolic,
1033 A: Symbolic,
1034{
1035 /// Read string representation starting from current position in formula.
1036 /// THIS WILL NOT PRINT ANY UNZIPPED PARTS OF THE FORMULA. Make sure to
1037 /// [`Formula::top_zip`] first if you want the whole formula.
1038 /// Printing is an easy way to see "where" you are in the formula.
1039 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1040 let mut outparam = String::new();
1041 self.tree.read_inorder(&mut outparam);
1042 write!(f, "{}", outparam)
1043 }
1044}
1045
1046impl<B, U, A> From<Tree<B, U, A>> for Formula<B, U, A>
1047where
1048 B: Symbolic,
1049 U: Symbolic,
1050 A: Symbolic,
1051{
1052 fn from(value: Tree<B, U, A>) -> Self {
1053 Formula {
1054 tree: value,
1055 zipper: Zipper::Top,
1056 }
1057 }
1058}