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

Add ability to split e-classes for easier visualization #14

Merged
merged 5 commits into from
Sep 9, 2024
Merged
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
117 changes: 113 additions & 4 deletions src/algorithms.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::EGraph;
use std::collections::HashMap;

use crate::{Class, ClassId, EGraph, Node, NodeId};

pub const MISSING_ARG_VALUE: &str = "·";

Expand Down Expand Up @@ -94,12 +96,12 @@ impl EGraph {
// 5. Remove leaf nodes from egraph, class data, and root eclasses
for (eclass, node_id) in &leaves {
// If this node has no parents, don't remove it, since it wasn't inlined
if node_to_parents.get(node_id).is_none() {
if !node_to_parents.contains_key(node_id) {
continue;
}
n_inlined += 1;
self.nodes.remove(node_id);
self.class_data.remove(eclass);
self.nodes.swap_remove(node_id);
self.class_data.swap_remove(eclass);
self.root_eclasses.retain(|root| root != eclass);
}
n_inlined
Expand All @@ -109,4 +111,111 @@ impl EGraph {
pub fn saturate_inline_leaves(&mut self) {
while self.inline_leaves() > 0 {}
}

/// Given some function `should_split`, after calling this method, all nodes where it is true will have at most
/// one other node in their e-class and if they have parents, will no other nodes in their e-class.
///
/// All nodes in an e-class shared with a split node will still be in a e-class with that node, but no longer
/// in an e-class with each other.
///
/// This is used to help make the visualization easier to parse, by breaking cycles and allowing these split nodes
/// to be later inlined into their parents with `inline_leaves`, which only applies when there is a single node in an
/// e-class.
///
/// For example, if we split on all "primitive" nodes or their wrappers, like Int(1), then those nodes
/// can then be inlined into their parents and are all nodes equal to them are no longer in single e-class,
/// making the graph layout easier to understand.
///
/// Note that `should_split` should only ever be true for either a single node, or no nodes, in an e-class.
/// If it is true for multiple nodes in an e-class, then this method will panic.
///
/// Another way to think about it is that any isomporphic function can be split, since if f(a) = f(b) then a = b,
/// in that case.
pub fn split_classes(&mut self, should_split: impl Fn(&NodeId, &Node) -> bool) {
// run till fixpoint since splitting a node might add more parents and require splitting the child down the line
let mut changed = true;
while changed {
changed = false;
// Mapping from class ID to all nodes that point to any node in that e-class
let parents: HashMap<ClassId, Vec<(NodeId, usize)>> =
self.nodes
.iter()
.fold(HashMap::new(), |mut parents, (node_id, node)| {
for (position, child) in node.children.iter().enumerate() {
let child_class = self.nodes[child].eclass.clone();
parents
.entry(child_class)
.or_default()
.push((node_id.clone(), position));
}
parents
});

for Class { id, nodes } in self.classes().clone().into_values() {
let (unique_nodes, other_nodes): (Vec<_>, Vec<_>) = nodes
.into_iter()
.partition(|node_id| should_split(node_id, &self.nodes[node_id]));
if unique_nodes.len() > 1 {
panic!(
"Multiple nodes in one e-class should be split. E-class: {:?} Nodes: {:?}",
id, unique_nodes
);
}
let unique_node = unique_nodes.into_iter().next();
let class_data = self.class_data.get(&id).cloned();
if let Some(unique_node_id) = unique_node {
let unique_node = self.nodes[&unique_node_id].clone();
let n_other_nodes = other_nodes.len();
let mut offset = 0;
if n_other_nodes == 0 {
continue;
}
// split out other nodes if there are multiple of them.
// Leave one node in this e-class and make new e-classes for remaining nodes
for other_node_id in other_nodes.into_iter().skip(1) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For other_nodes[0], which is skipped, while you don't need to create a new e-class for it, don't you need to merge it with a clone of unique_node?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, I don't believe so because we leave the original unique node there with it.

changed = true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is changed set to true here (in addition to below where parent E-nodes are updated).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the logic to only trigger changed if there are any parents and we make a new node. Changed so only be true if the e-graph was mutated.

// use same ID for new class and new node added to that class
let new_id = format!("split-{}-{}", offset, unique_node_id);
offset += 1;
let new_class_id: ClassId = new_id.clone().into();
// Copy the class data if it exists
if let Some(class_data) = &class_data {
self.class_data
.insert(new_class_id.clone(), class_data.clone());
}
// Change the e-class of the other node
self.nodes[&other_node_id].eclass = new_class_id.clone();
// Create a new unique node with the same data
let mut new_unique_node = unique_node.clone();
new_unique_node.eclass = new_class_id;
self.nodes.insert(new_id.into(), new_unique_node);
}
// If there are other nodes, then make one more copy and point all the parents at that
let parents = parents.get(&id).cloned().unwrap_or_default();
if parents.is_empty() {
continue;
}
changed = true;
let new_id = format!("split-{}-{}", offset, unique_node_id);
let new_class_id: ClassId = new_id.clone().into();
// Copy the class data if it exists
if let Some(class_data) = &class_data {
self.class_data
.insert(new_class_id.clone(), class_data.clone());
}
// Create a new unique node with the same data
let mut new_unique_node = unique_node.clone();
new_unique_node.eclass = new_class_id;
self.nodes.insert(new_id.clone().into(), new_unique_node);
for (parent_id, position) in parents {
// Change the child of the parent to the new node
self.nodes.get_mut(&parent_id).unwrap().children[position] =
new_id.clone().into();
}
}
}
// reset the classes computation
self.once_cell_classes.take();
}
}
}
Loading