diff --git a/Cargo.lock b/Cargo.lock index d0817d1c7a..49cbf71ee7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1265,6 +1265,12 @@ dependencies = [ "adler2", ] +[[package]] +name = "multipeek" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6b1cf1c2ae7c8c3898cbf8354ee836bc7037e35592d3739a9901d53c97b6a2" + [[package]] name = "ndk-context" version = "0.1.1" @@ -2286,6 +2292,8 @@ dependencies = [ name = "tree-morph" version = "0.1.0" dependencies = [ + "multipeek", + "rand", "uniplate", ] diff --git a/crates/tree_morph/Cargo.toml b/crates/tree_morph/Cargo.toml index 738d3a2fd1..af11760134 100644 --- a/crates/tree_morph/Cargo.toml +++ b/crates/tree_morph/Cargo.toml @@ -4,7 +4,9 @@ version = "0.1.0" edition = "2021" [dependencies] -uniplate = { version = "0.1.5" } +multipeek = "0.1.2" +rand = "0.8.5" +uniplate = { version = "0.1.0" } [lints] diff --git a/crates/tree_morph/src/commands.rs b/crates/tree_morph/src/commands.rs index 6442a78bfb..bdbfd56a1d 100644 --- a/crates/tree_morph/src/commands.rs +++ b/crates/tree_morph/src/commands.rs @@ -1,7 +1,7 @@ use std::collections::VecDeque; use uniplate::Uniplate; -pub enum Command +enum Command where T: Uniplate, { @@ -21,7 +21,7 @@ impl Commands where T: Uniplate, { - pub fn new() -> Self { + pub(crate) fn new() -> Self { Self { commands: VecDeque::new(), } diff --git a/crates/tree_morph/src/helpers.rs b/crates/tree_morph/src/helpers.rs index 95c47e3efb..ef5946bcb9 100644 --- a/crates/tree_morph/src/helpers.rs +++ b/crates/tree_morph/src/helpers.rs @@ -1,7 +1,29 @@ -use uniplate::Uniplate; +use std::{fmt::Display, io::Write, sync::Arc}; use crate::{Reduction, Rule}; +use multipeek::multipeek; +use uniplate::Uniplate; +/// Returns the first result if the iterator has only one, otherwise calls `select`. +pub(crate) fn one_or_select( + select: impl Fn(&T, &mut dyn Iterator)>) -> Option>, + t: &T, + rs: &mut dyn Iterator)>, +) -> Option> +where + T: Uniplate, + R: Rule, +{ + let mut rs = multipeek(rs); + if rs.peek_nth(1).is_none() { + return rs.next().map(|(_, r)| r); + } + select(t, &mut rs) +} + +/// Returns the first available `Reduction` if there is one, otherwise returns `None`. +/// +/// This is a good default selection strategy, especially when you expect only one possible result. pub fn select_first( _: &T, rs: &mut dyn Iterator)>, @@ -13,5 +35,118 @@ where rs.next().map(|(_, r)| r) } -// TODO: Add more selection strategies (e.g. random, smallest subtree, ask the user for input, etc.) -// The engine will also have to use a new function which only calls a selector function if there is >1 result +/// Select the first result or panic if there is more than one. +/// +/// This is useful when you expect exactly one rule to be applicable in all cases. +pub fn select_first_or_panic( + t: &T, + rs: &mut dyn Iterator)>, +) -> Option> +where + T: Uniplate + Display, + R: Rule, +{ + let mut rs = multipeek(rs); + if rs.peek_nth(1).is_some() { + // TODO (Felix) Log list of rules + panic!("Multiple rules applicable to expression \"{}\"", t); + } + rs.next().map(|(_, r)| r) +} + +macro_rules! select_prompt { + () => { + "--- Current Expression --- +{} + +--- Rules --- +{} + +--- +q No change + Apply rule n + +:" + }; +} + +pub fn select_user_input( + t: &T, + rs: &mut dyn Iterator)>, +) -> Option> +where + T: Uniplate + Display, + R: Rule + Display, +{ + let mut choices: Vec<_> = rs.collect(); + + let rules = choices + .iter() + .enumerate() + .map(|(i, (r, Reduction { new_tree, .. }))| { + format!( + "{}. {} + ~> {}", + i + 1, + r, + new_tree + ) + }) + .collect::>() + .join("\n\n"); + + loop { + print!(select_prompt!(), t, rules); + std::io::stdout().flush().unwrap(); // Print the : on same line + + let mut line = String::new(); + std::io::stdin().read_line(&mut line).unwrap(); + + match line.trim() { + "q" => return None, + n => { + if let Ok(n) = n.parse::() { + if n > 0 && n <= choices.len() { + let ret = choices.swap_remove(n - 1).1; + return Some(ret); + } + } + } + } + } +} + +/// Selects a random `Reduction` from the iterator. +pub fn select_random( + _: &T, + rs: &mut dyn Iterator)>, +) -> Option> +where + T: Uniplate, + R: Rule, +{ + use rand::seq::IteratorRandom; + let mut rng = rand::thread_rng(); + rs.choose(&mut rng).map(|(_, r)| r) +} + +/// Selects the `Reduction` which results in the smallest subtree. +/// +/// Subtree size is determined by maximum depth. +/// Among trees with the same depth, the first in the iterator order is selected. +pub fn select_smallest_subtree( + _: &T, + rs: &mut dyn Iterator)>, +) -> Option> +where + T: Uniplate, + R: Rule, +{ + rs.min_by_key(|(_, r)| { + r.new_tree.cata(Arc::new(|_, cs: Vec| { + // Max subtree height + 1 + cs.iter().max().unwrap_or(&0) + 1 + })) + }) + .map(|(_, r)| r) +} diff --git a/crates/tree_morph/src/reduce.rs b/crates/tree_morph/src/reduce.rs index 8d78f84297..13d3830033 100644 --- a/crates/tree_morph/src/reduce.rs +++ b/crates/tree_morph/src/reduce.rs @@ -1,4 +1,4 @@ -use crate::{Commands, Reduction, Rule}; +use crate::{helpers::one_or_select, Commands, Reduction, Rule}; use uniplate::Uniplate; // TODO: (Felix) dirty/clean optimisation: replace tree with a custom tree structure, @@ -96,7 +96,8 @@ where { reduce( |commands, subtree, meta| { - let selection = select( + let selection = one_or_select( + &select, subtree, &mut rules.iter().filter_map(|rule| { Reduction::apply_transform(|c, t, m| rule.apply(c, t, m), subtree, meta) diff --git a/crates/tree_morph/tests/depend_meta.rs b/crates/tree_morph/tests/depend_meta.rs index 8814b778a2..95b3644004 100644 --- a/crates/tree_morph/tests/depend_meta.rs +++ b/crates/tree_morph/tests/depend_meta.rs @@ -1,4 +1,4 @@ -//! Here we test an interesting side-effect case, with rules return a reduction based on the metadata. +//! Here we test an interesting side-effect case, with rules which return a reduction based on a metadata field. //! These rules will not be run a second time if no other rule applies to the same node, which might be unexpected. use tree_morph::*; @@ -16,7 +16,7 @@ fn transform(cmd: &mut Commands, expr: &Expr, meta: &bool) -> Option if *meta { return Some(Expr::Two); } else { - cmd.mut_meta(|m| *m = true); // The next application of this rule should + cmd.mut_meta(|m| *m = true); // The next application of this rule will apply } } None