use std::collections::{HashMap, HashSet};
use crate::matcher::{match_trees, tree_similarity};
use crate::types::{CstNode, ListOrdering, MergeResult, MergeScenario, NodeId};
#[derive(Debug)]
pub enum AmalgamResult {
Merged(CstNode),
Conflict {
base: CstNode,
left: CstNode,
right: CstNode,
},
}
pub fn amalgamate(scenario: &MergeScenario<&CstNode>) -> AmalgamResult {
let bl_matches = match_trees(scenario.base, scenario.left);
let br_matches = match_trees(scenario.base, scenario.right);
let lr_matches = match_trees(scenario.left, scenario.right);
let bl_map: HashMap<NodeId, NodeId> = bl_matches.iter().map(|p| (p.left, p.right)).collect();
let br_map: HashMap<NodeId, NodeId> = br_matches.iter().map(|p| (p.left, p.right)).collect();
let lr_map: HashMap<NodeId, NodeId> = lr_matches.iter().map(|p| (p.left, p.right)).collect();
amalgamate_node(
scenario.base,
scenario.left,
scenario.right,
&bl_map,
&br_map,
&lr_map,
)
}
fn amalgamate_node(
base: &CstNode,
left: &CstNode,
right: &CstNode,
bl_map: &HashMap<NodeId, NodeId>,
br_map: &HashMap<NodeId, NodeId>,
lr_map: &HashMap<NodeId, NodeId>,
) -> AmalgamResult {
if base.structurally_equal(left) && base.structurally_equal(right) {
return AmalgamResult::Merged(base.clone());
}
if base.structurally_equal(right) {
return AmalgamResult::Merged(left.clone());
}
if base.structurally_equal(left) {
return AmalgamResult::Merged(right.clone());
}
if left.structurally_equal(right) {
return AmalgamResult::Merged(left.clone());
}
match (base, left, right) {
(CstNode::Leaf { .. }, CstNode::Leaf { .. }, CstNode::Leaf { .. }) => {
AmalgamResult::Conflict {
base: base.clone(),
left: left.clone(),
right: right.clone(),
}
}
_ if !base.is_leaf() && !left.is_leaf() && !right.is_leaf() => {
amalgamate_children(base, left, right, bl_map, br_map, lr_map)
}
_ => AmalgamResult::Conflict {
base: base.clone(),
left: left.clone(),
right: right.clone(),
},
}
}
fn amalgamate_children(
base: &CstNode,
left: &CstNode,
right: &CstNode,
bl_map: &HashMap<NodeId, NodeId>,
br_map: &HashMap<NodeId, NodeId>,
lr_map: &HashMap<NodeId, NodeId>,
) -> AmalgamResult {
let base_children = base.children();
let left_children = left.children();
let right_children = right.children();
if let CstNode::List {
ordering: ListOrdering::Unordered,
..
} = base
{
return amalgamate_unordered(base, left, right, bl_map, br_map);
}
let bl_child_map = build_child_match_map(base_children, left_children, bl_map);
let br_child_map = build_child_match_map(base_children, right_children, br_map);
let mut merged_children = Vec::new();
let mut has_conflict = false;
let mut conflict_base = base.clone();
let mut conflict_left = left.clone();
let mut conflict_right = right.clone();
let mut used_left: HashSet<NodeId> = HashSet::new();
let mut used_right: HashSet<NodeId> = HashSet::new();
for base_child in base_children {
let left_match = bl_child_map.get(&base_child.id());
let right_match = br_child_map.get(&base_child.id());
match (left_match, right_match) {
(Some(lc), Some(rc)) => {
used_left.insert(lc.id());
used_right.insert(rc.id());
match amalgamate_node(base_child, lc, rc, bl_map, br_map, lr_map) {
AmalgamResult::Merged(node) => merged_children.push(node),
AmalgamResult::Conflict {
base: b,
left: l,
right: r,
} => {
has_conflict = true;
conflict_base = b;
conflict_left = l;
conflict_right = r;
merged_children.push((*lc).clone());
}
}
}
(Some(lc), None) => {
used_left.insert(lc.id());
if base_child.structurally_equal(lc) {
} else {
has_conflict = true;
conflict_base = base_child.clone();
conflict_left = (*lc).clone();
conflict_right = CstNode::Leaf {
id: 0,
kind: "deleted".into(),
value: String::new(),
};
}
}
(None, Some(rc)) => {
used_right.insert(rc.id());
if base_child.structurally_equal(rc) {
} else {
has_conflict = true;
conflict_base = base_child.clone();
conflict_left = CstNode::Leaf {
id: 0,
kind: "deleted".into(),
value: String::new(),
};
conflict_right = (*rc).clone();
}
}
(None, None) => {}
}
}
for lc in left_children {
if !used_left.contains(&lc.id()) && !bl_child_map.values().any(|v| v.id() == lc.id()) {
merged_children.push(lc.clone());
}
}
for rc in right_children {
if !used_right.contains(&rc.id()) && !br_child_map.values().any(|v| v.id() == rc.id()) {
merged_children.push(rc.clone());
}
}
if has_conflict {
AmalgamResult::Conflict {
base: conflict_base,
left: conflict_left,
right: conflict_right,
}
} else {
let merged = reconstruct_node(base, merged_children);
AmalgamResult::Merged(merged)
}
}
fn amalgamate_unordered(
base: &CstNode,
left: &CstNode,
right: &CstNode,
_bl_map: &HashMap<NodeId, NodeId>,
_br_map: &HashMap<NodeId, NodeId>,
) -> AmalgamResult {
let base_children = base.children();
let left_children = left.children();
let right_children = right.children();
let mut result_children = Vec::new();
let mut used_left: HashSet<usize> = HashSet::new();
let mut used_right: HashSet<usize> = HashSet::new();
for bc in base_children {
let left_match = left_children
.iter()
.enumerate()
.find(|(idx, lc)| !used_left.contains(idx) && bc.structurally_equal(lc));
let right_match = right_children
.iter()
.enumerate()
.find(|(idx, rc)| !used_right.contains(idx) && bc.structurally_equal(rc));
match (left_match, right_match) {
(Some((li, _)), Some((ri, _))) => {
used_left.insert(li);
used_right.insert(ri);
result_children.push(bc.clone());
}
(Some((li, _)), None) => {
used_left.insert(li);
}
(None, Some((ri, _))) => {
used_right.insert(ri);
}
(None, None) => {
}
}
}
for (i, lc) in left_children.iter().enumerate() {
if !used_left.contains(&i) {
result_children.push(lc.clone());
}
}
for (i, rc) in right_children.iter().enumerate() {
if !used_right.contains(&i) {
let already_added = result_children.iter().any(|c| c.structurally_equal(rc));
if !already_added {
result_children.push(rc.clone());
}
}
}
AmalgamResult::Merged(reconstruct_node(base, result_children))
}
fn build_child_match_map<'a>(
base_children: &[CstNode],
other_children: &'a [CstNode],
match_map: &HashMap<NodeId, NodeId>,
) -> HashMap<NodeId, &'a CstNode> {
let other_by_id: HashMap<NodeId, &CstNode> =
other_children.iter().map(|c| (c.id(), c)).collect();
let mut result = HashMap::new();
for bc in base_children {
if let Some(&other_id) = match_map.get(&bc.id())
&& let Some(other_node) = other_by_id.get(&other_id)
{
result.insert(bc.id(), *other_node);
}
}
let matched_other: HashSet<NodeId> = result.values().map(|n| n.id()).collect();
for bc in base_children {
if result.contains_key(&bc.id()) {
continue;
}
let best = other_children
.iter()
.filter(|oc| !matched_other.contains(&oc.id()))
.filter(|oc| oc.kind() == bc.kind())
.max_by_key(|oc| tree_similarity(bc, oc));
if let Some(matched) = best
&& tree_similarity(bc, matched) > 0
{
result.insert(bc.id(), matched);
}
}
result
}
fn reconstruct_node(template: &CstNode, children: Vec<CstNode>) -> CstNode {
match template {
CstNode::Leaf { .. } => template.clone(),
CstNode::Constructed { id, kind, .. } => CstNode::Constructed {
id: *id,
kind: kind.clone(),
children,
},
CstNode::List {
id, kind, ordering, ..
} => CstNode::List {
id: *id,
kind: kind.clone(),
ordering: *ordering,
children,
},
}
}
pub fn amalgam_to_merge_result(result: &AmalgamResult) -> MergeResult {
match result {
AmalgamResult::Merged(node) => MergeResult::Resolved(node.to_source()),
AmalgamResult::Conflict { base, left, right } => MergeResult::Conflict {
base: base.to_source(),
left: left.to_source(),
right: right.to_source(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::ListOrdering;
fn leaf(id: usize, val: &str) -> CstNode {
CstNode::Leaf {
id,
kind: "ident".into(),
value: val.into(),
}
}
#[allow(dead_code)]
fn list(id: usize, children: Vec<CstNode>) -> CstNode {
CstNode::List {
id,
kind: "block".into(),
ordering: ListOrdering::Ordered,
children,
}
}
fn unordered_list(id: usize, children: Vec<CstNode>) -> CstNode {
CstNode::List {
id,
kind: "import_list".into(),
ordering: ListOrdering::Unordered,
children,
}
}
#[test]
fn test_no_change() {
let base = leaf(1, "x");
let left = leaf(2, "x");
let right = leaf(3, "x");
let scenario = MergeScenario::new(&base, &left, &right);
let result = amalgamate(&scenario);
assert!(matches!(result, AmalgamResult::Merged(_)));
}
#[test]
fn test_left_only_change() {
let base = leaf(1, "x");
let left = leaf(2, "y");
let right = leaf(3, "x");
let scenario = MergeScenario::new(&base, &left, &right);
let result = amalgamate(&scenario);
match result {
AmalgamResult::Merged(node) => assert_eq!(node.leaf_value(), Some("y")),
_ => panic!("expected merged"),
}
}
#[test]
fn test_both_same_change() {
let base = leaf(1, "x");
let left = leaf(2, "z");
let right = leaf(3, "z");
let scenario = MergeScenario::new(&base, &left, &right);
let result = amalgamate(&scenario);
match result {
AmalgamResult::Merged(node) => assert_eq!(node.leaf_value(), Some("z")),
_ => panic!("expected merged"),
}
}
#[test]
fn test_true_conflict() {
let base = leaf(1, "x");
let left = leaf(2, "y");
let right = leaf(3, "z");
let scenario = MergeScenario::new(&base, &left, &right);
let result = amalgamate(&scenario);
assert!(matches!(result, AmalgamResult::Conflict { .. }));
}
#[test]
fn test_unordered_merge_additions() {
let base = unordered_list(1, vec![leaf(2, "a"), leaf(3, "b")]);
let left = unordered_list(4, vec![leaf(5, "a"), leaf(6, "b"), leaf(7, "c")]);
let right = unordered_list(8, vec![leaf(9, "a"), leaf(10, "b"), leaf(11, "d")]);
let scenario = MergeScenario::new(&base, &left, &right);
let result = amalgamate(&scenario);
match result {
AmalgamResult::Merged(node) => {
assert_eq!(node.children().len(), 4);
}
_ => panic!("expected unordered merge to succeed"),
}
}
}