Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Treemorph dirty clean #593

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions crates/tree_morph/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ version = "0.1.0"
edition = "2021"

[dependencies]
im = "15.1.0"
multipeek = "0.1.2"
rand = "0.8.5"
uniplate = { version = "0.1.0" }

uniplate = "0.1.5"

[lints]
workspace = true

[features]
examples = []
13 changes: 13 additions & 0 deletions crates/tree_morph/src/examples.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
use uniplate::derive::Uniplate;

#[derive(Debug, Clone, PartialEq, Eq, Uniplate)]
pub enum Expr {
Nothing,
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
Mul(Box<Expr>, Box<Expr>),
Div(Box<Expr>, Box<Expr>),
Val(i32),
Var(String),
Neg(Box<Expr>),
}
10 changes: 9 additions & 1 deletion crates/tree_morph/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{fmt::Display, io::Write, sync::Arc};

use crate::{Reduction, Rule};
use crate::prelude::*;
use multipeek::multipeek;
use uniplate::Uniplate;

Expand All @@ -21,6 +21,14 @@ where
select(t, &mut rs)
}

/// Returns the number of nodes in the given tree.
pub(crate) fn tree_size<T>(tree: &T) -> usize
where
T: Uniplate,
{
tree.cata(Arc::new(|_, sizes| sizes.iter().sum::<usize>() + 1))
}

/// 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.
Expand Down
29 changes: 19 additions & 10 deletions crates/tree_morph/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
//# TODO (Felix): choose a more concise example
//!
//! ```rust
//! use tree_morph::*;
//! use tree_morph::prelude::*;
//! use uniplate::derive::Uniplate;
//!
//! #[derive(Debug, Clone, PartialEq, Eq, Uniplate)]
Expand Down Expand Up @@ -133,7 +133,7 @@
//! // Ordering is important here: we evaluate first (1), then reduce (2..6), then change form (7)
//! let rules = [Eval, AddZero, AddSame, MulOne, MulZero, DoubleNeg, Associativity];
//!
//! let (expr, _) = reduce_with_rules(&rules, helpers::select_first, expr, ());
//! let (expr, _) = reduce_with_rules(&rules, select_first, expr, ());
//! assert_eq!(expr, Mul(bx(Val(4)), bx(Var("x".to_string()))));
//! }
//!
Expand All @@ -149,13 +149,22 @@
//! These functions can then be defined elsewhere for better organization.
//!

mod commands;
pub mod commands;
pub mod helpers;
mod reduce;
mod reduction;
mod rule;
pub mod reduce;
pub mod reduction;
pub mod rule;
pub mod zipper;

pub use commands::Commands;
pub use reduce::{reduce, reduce_with_rules};
pub use reduction::Reduction;
pub use rule::Rule;
pub mod prelude {
use super::*;

pub use commands::Commands;
pub use helpers::select_first;
pub use reduce::{reduce, reduce_with_rule_groups, reduce_with_rules};
pub use reduction::Reduction;
pub use rule::Rule;
}

#[cfg(any(test, feature = "examples"))]
pub mod examples;
93 changes: 45 additions & 48 deletions crates/tree_morph/src/reduce.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{helpers::one_or_select, Commands, Reduction, Rule};
use crate::{helpers::one_or_select, prelude::*};
use uniplate::Uniplate;

// TODO: (Felix) dirty/clean optimisation: replace tree with a custom tree structure,
Expand All @@ -10,6 +10,8 @@ use uniplate::Uniplate;
// TODO: (Felix) add "control" rules; e.g. ignore a subtree to a certain depth?
// test by ignoring everything once a metadata field is set? e.g. "reduce until contains X"

/// TODO: rewrite for new interface
///
/// Exhaustively reduce a tree using a given transformation function.
///
/// The transformation function is called on each node in the tree (in left-most, outer-most order) along with
Expand All @@ -31,44 +33,31 @@ use uniplate::Uniplate;
///
/// # Returns
/// A tuple containing the reduced tree and the final metadata.
pub fn reduce<T, M, F>(transform: F, mut tree: T, mut meta: M) -> (T, M)
pub fn reduce<T, M, F>(transforms: &[F], mut tree: T, mut meta: M) -> (T, M)
where
T: Uniplate,
F: Fn(&mut Commands<T, M>, &T, &M) -> Option<T>,
{
// Apply the transformation to the tree until no more changes are made
while let Some(mut reduction) = reduce_iteration(&transform, &tree, &meta) {
// Apply reduction side-effects
(tree, meta) = reduction.commands.apply(reduction.new_tree, meta);
}
(tree, meta)
}
let mut new_tree = tree;
'main: loop {
tree = new_tree;
for transform in transforms.iter() {
// Try each transform on the entire tree before moving to the next
for (node, ctx) in tree.contexts() {
let red_opt = Reduction::apply_transform(transform, &node, &meta);

fn reduce_iteration<T, M, F>(transform: &F, subtree: &T, meta: &M) -> Option<Reduction<T, M>>
where
T: Uniplate,
F: Fn(&mut Commands<T, M>, &T, &M) -> Option<T>,
{
// Try to apply the transformation to the current node
let reduction = Reduction::apply_transform(transform, subtree, meta);
if reduction.is_some() {
return reduction;
}
if let Some(mut red) = red_opt {
(new_tree, meta) = red.commands.apply(ctx(red.new_tree), meta);

// Try to call the transformation on the children of the current node
// If successful, return the new subtree
let mut children = subtree.children();
for c in children.iter_mut() {
if let Some(reduction) = reduce_iteration(transform, c, meta) {
*c = reduction.new_tree;
return Some(Reduction {
new_tree: subtree.with_children(children),
..reduction
});
// Restart with the first transform every time a change is made
continue 'main;
}
}
}
// All transforms were attempted without change
break;
}

None
(tree, meta)
}

/// Exhaustively reduce a tree by applying the given rules at each node.
Expand All @@ -94,23 +83,31 @@ where
R: Rule<T, M>,
S: Fn(&T, &mut dyn Iterator<Item = (&R, Reduction<T, M>)>) -> Option<Reduction<T, M>>,
{
reduce(
|commands, subtree, meta| {
let selection = one_or_select(
&select,
subtree,
&mut rules.iter().filter_map(|rule| {
reduce_with_rule_groups(&[rules], select, tree, meta)
}

pub fn reduce_with_rule_groups<T, M, R, S>(groups: &[&[R]], select: S, tree: T, meta: M) -> (T, M)
where
T: Uniplate,
R: Rule<T, M>,
S: Fn(&T, &mut dyn Iterator<Item = (&R, Reduction<T, M>)>) -> Option<Reduction<T, M>>,
{
let transforms: Vec<_> = groups
.iter()
.map(|group| {
|commands: &mut Commands<T, M>, subtree: &T, meta: &M| {
let applicable = &mut group.iter().filter_map(|rule| {
Reduction::apply_transform(|c, t, m| rule.apply(c, t, m), subtree, meta)
.map(|r| (rule, r))
}),
);
selection.map(|r| {
// Ensure commands used by the engine are the ones resulting from this reduction
*commands = r.commands;
r.new_tree
})
},
tree,
meta,
)
});
let selection = one_or_select(&select, subtree, applicable);
selection.map(|r| {
// Ensure commands used by the engine are the ones resulting from this reduction
*commands = r.commands;
r.new_tree
})
}
})
.collect();
reduce(&transforms, tree, meta)
}
2 changes: 1 addition & 1 deletion crates/tree_morph/src/reduction.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::Commands;
use crate::commands::Commands;
use uniplate::Uniplate;

pub struct Reduction<T, M>
Expand Down
2 changes: 1 addition & 1 deletion crates/tree_morph/src/rule.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::Commands;
use crate::commands::Commands;
use uniplate::Uniplate;

pub trait Rule<T, M>
Expand Down
121 changes: 121 additions & 0 deletions crates/tree_morph/src/skel.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use std::rc::Rc;

use uniplate::{Tree, Uniplate};

/// Additional metadata associated with each tree node.
#[derive(Debug, Clone, PartialEq, Eq)]
struct Meta {
/// Transforms at and after this index should be applied.
/// Those before it have already been tried with no change.
clean_before: usize,
}

impl Meta {
fn new() -> Self {
Self { clean_before: 0 }
}
}

/// Wraps around a tree and associates additional metadata with each node for rewriter optimisations.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Skel<T>
where
T: Uniplate,
{
pub node: Rc<T>,
pub meta: Meta,
pub children: Vec<Skel<T>>,
}

impl<T> Skel<T>
where
T: Uniplate,
{
/// Construct a new `Skel` wrapper around an entire tree.
pub fn new(node: T) -> Self {
Self::_new(Rc::new(node))
}

fn _new(node: Rc<T>) -> Self {
Self {
node: Rc::clone(&node),
meta: Meta::new(),
children: node
.children()
.into_iter()
.map(|child| Skel::_new(Rc::new(child)))
.collect(),
}
}
}

impl<T> Uniplate for Skel<T>
where
T: Uniplate,
{
// Implemented here as Uniplate doesn't yet support this struct
fn uniplate(&self) -> (Tree<Self>, Box<dyn Fn(Tree<Self>) -> Self>) {
let node = Rc::clone(&self.node);
let meta = self.meta.clone();

let tree = Tree::Many(im::Vector::from_iter(
self.children.clone().into_iter().map(Tree::One),
));

let ctx = Box::new(move |tr| {
let Tree::Many(trs) = tr else { panic!() };
let children = trs
.into_iter()
.map(|ch| {
let Tree::One(sk) = ch else { panic!() };
sk
})
.collect();
Skel {
node: node.clone(),
meta: meta.clone(),
children,
}
});

(tree, ctx)
}
}

#[cfg(test)]
mod tests {
use std::sync::Arc;

use super::*;

use im::Vector;
use uniplate::derive::Uniplate;
use uniplate::Uniplate;

#[derive(Debug, Clone, PartialEq, Eq, Uniplate)]
enum Expr {
A,
B,
P(Box<Expr>, Box<Expr>),
}

#[test]
fn test_skel_reconstruct() {
let tree = Expr::P(
Box::new(Expr::P(
Box::new(Expr::P(Box::new(Expr::A), Box::new(Expr::A))),
Box::new(Expr::B),
)),
Box::new(Expr::B),
);

let skel = Skel::new(tree.clone());
let reconstructed = skel.cata(Arc::new(|sk: Skel<Expr>, chs| {
sk.node.with_children(Vector::from(chs))
}));

// println!("Tree: {:?}", tree);
// println!("Skel: {:?}", skel);
assert_eq!(tree, reconstructed);
}
}
Loading