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 2 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
107 changes: 103 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,101 @@ impl EGraph {
pub fn saturate_inline_leaves(&mut self) {
while self.inline_leaves() > 0 {}
}

/// Splits e-classes with these nodes into multiple e-classes, copying the node into each. Also will create
/// a new e-class for each node pointing to any e-class with a node that should be split.
///
/// Note that if any of these nodes appear twice in an e-class then it will panic.
///
/// Class data will be copied to new nodes.
///
/// This can be used for example to make multiple e-classes for all nodes equivalent to i64(0), to make it easier
/// to visualize this.
pub fn split_e_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

Choose a reason for hiding this comment

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

I found myself reading the description and the implementation several times before understanding what this does. The "unique node" is essentially a representative that every duplicated e-class would contain, and that's why it needs to be unique, is this right? I think the documentation might be clearer if it is narrated from an application point of view (e.g., what it tries to achieve at a high level) or describes some end-to-end use scenarios.

Choose a reason for hiding this comment

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

nits: eclasses or classes seems to be more consistent with the rest of the library than e_classes

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you! I updated the comment to try to explain it a bit more. Happy for more feedback about parts that are confusing.

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().values() {
let mut other_nodes = Vec::new();
let mut unique_node = None;
for node_id in nodes {
let node = self.nodes[node_id].clone();

Choose a reason for hiding this comment

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

Nits: the logic can be probably simplified with parition

Copy link
Member Author

Choose a reason for hiding this comment

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

Thank you, yep that was much clearer.

if should_split(node_id, &node) {
if let Some((other_node_id, other_node)) = unique_node {
panic!(
"Multiple nodes in one e-class should be split. E-class: {:} Node 1: {:?} {:?} Node 2: {:?} {:?}",
id, node_id, node, other_node_id, other_node
);
}
unique_node = Some((node_id, node));
} else {
other_nodes.push(node_id);
}
}
let class_data = self.class_data.get(id).cloned();
if let Some((unique_node_id, unique_node)) = unique_node {
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) {
changed = true;

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.

// use same ID for new class and new node added to that class

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.

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 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.get(id).cloned().unwrap_or_default() {
changed = true;
// 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