From 04d3e916ba523c2dabcf81836094c4cb254ec43e Mon Sep 17 00:00:00 2001 From: Wilfred Hughes Date: Sun, 12 Sep 2021 21:01:54 -0700 Subject: [PATCH] Add benchmark file (takes 3-4 seconds today) --- sample_files/slow_after.rs | 556 +++++++++++++++++++++++++++++++++++ sample_files/slow_before.rs | 571 ++++++++++++++++++++++++++++++++++++ 2 files changed, 1127 insertions(+) create mode 100644 sample_files/slow_after.rs create mode 100644 sample_files/slow_before.rs diff --git a/sample_files/slow_after.rs b/sample_files/slow_after.rs new file mode 100644 index 000000000..0b5367859 --- /dev/null +++ b/sample_files/slow_after.rs @@ -0,0 +1,556 @@ +use std::cmp::Ordering; +use std::collections::BinaryHeap; + +use crate::syntax::{ChangeKind, Syntax}; +use rustc_hash::{FxHashMap, FxHashSet}; +use Edge::*; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct Vertex<'a> { + lhs_syntax: Option<&'a Syntax<'a>>, + rhs_syntax: Option<&'a Syntax<'a>>, +} + +impl<'a> Vertex<'a> { + fn is_end(&self) -> bool { + self.lhs_syntax.is_none() && self.rhs_syntax.is_none() + } +} + +// Rust requires that PartialEq, PartialOrd and Ord agree. +// https://doc.rust-lang.org/std/cmp/trait.Ord.html +// +// For `Vertex`, we want to compare by distance in a priority queue, but +// equality should only consider LHS/RHS node when deciding if we've +// visited a vertex. We define separate wrappers for these two use +// cases. +#[derive(Debug)] +struct OrdVertex<'a> { + distance: i64, + v: Vertex<'a>, +} + +impl<'a> PartialOrd for OrdVertex<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl<'a> Ord for OrdVertex<'a> { + fn cmp(&self, other: &Self) -> Ordering { + self.distance.cmp(&other.distance) + } +} + +impl<'a> PartialEq for OrdVertex<'a> { + fn eq(&self, other: &Self) -> bool { + self.distance == other.distance + } +} +impl<'a> Eq for OrdVertex<'a> {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +enum Edge { + StartNode, + UnchangedNode, + UnchangedDelimiter, + NovelAtomLHS { same_line: bool }, + NovelAtomRHS { same_line: bool }, + NovelDelimiterLHS, + NovelDelimiterRHS, +} + +impl Edge { + fn cost(&self) -> i64 { + match self { + StartNode => 0, + // Matching nodes is always best. + UnchangedNode => 0, + // Matching an outer delimiter is good. + UnchangedDelimiter => -1, + // Otherwise, we've added/removed a node. Prefer novel + // nodes on the same line, to reduce the risk of sliders. + NovelAtomLHS { same_line } | NovelAtomRHS { same_line } => { + if *same_line { + -2 + } else { + -3 + } + } + NovelDelimiterLHS => -2, + NovelDelimiterRHS => -2, + } + } +} + +fn shortest_path(start: Vertex) -> Vec<(Edge, Vertex)> { + let mut heap = BinaryHeap::new(); + heap.push(OrdVertex { + distance: 0, + v: start.clone(), + }); + + let mut visited = FxHashSet::default(); + let mut predecessors: FxHashMap = FxHashMap::default(); + + loop { + match heap.pop() { + Some(OrdVertex { distance, v }) => { + if v.is_end() { + break; + } + + if visited.contains(&v) { + continue; + } + for (edge, new_v) in neighbours(&v) { + if !predecessors.contains_key(&new_v) { + predecessors.insert(new_v.clone(), (edge, v.clone())); + heap.push(OrdVertex { + distance: distance + edge.cost(), + v: new_v, + }); + } + } + + visited.insert(v); + } + None => panic!("Ran out of graph nodes before reaching end"), + } + } + + let mut current = Vertex { + lhs_syntax: None, + rhs_syntax: None, + }; + let mut res: Vec<(Edge, Vertex)> = vec![]; + loop { + match predecessors.remove(¤t) { + Some((edge, node)) => { + res.push((edge, node.clone())); + current = node; + } + None => { + res.push((StartNode, current.clone())); + break; + } + } + } + + res.reverse(); + res +} + +fn neighbours<'a>(v: &Vertex<'a>) -> Vec<(Edge, Vertex<'a>)> { + let mut res = vec![]; + + if let (Some(lhs_syntax), Some(rhs_syntax)) = (&v.lhs_syntax, &v.rhs_syntax) { + if lhs_syntax.equal_content(rhs_syntax) { + // Both nodes are equal, the happy case. + res.push(( + UnchangedNode, + Vertex { + lhs_syntax: lhs_syntax.get_next(), + rhs_syntax: rhs_syntax.get_next(), + }, + )); + } + + if let ( + Syntax::List { + open_content: lhs_open_content, + close_content: lhs_close_content, + children: lhs_children, + .. + }, + Syntax::List { + open_content: rhs_open_content, + close_content: rhs_close_content, + children: rhs_children, + .. + }, + ) = (lhs_syntax, rhs_syntax) + { + if lhs_open_content == rhs_open_content && lhs_close_content == rhs_close_content { + let lhs_next = if lhs_children.is_empty() { + lhs_syntax.get_next() + } else { + Some(lhs_children[0]) + }; + let rhs_next = if rhs_children.is_empty() { + rhs_syntax.get_next() + } else { + Some(rhs_children[0]) + }; + res.push(( + UnchangedDelimiter, + Vertex { + lhs_syntax: lhs_next, + rhs_syntax: rhs_next, + }, + )); + } + } + } + + if let Some(lhs_syntax) = &v.lhs_syntax { + match lhs_syntax { + // Step over this novel atom. + Syntax::Atom { .. } => { + res.push(( + NovelAtomLHS { same_line: false }, + Vertex { + lhs_syntax: lhs_syntax.get_next(), + rhs_syntax: v.rhs_syntax, + }, + )); + } + // Step into this partially/fully novel list. + Syntax::List { children, .. } => { + let lhs_next = if children.is_empty() { + lhs_syntax.get_next() + } else { + Some(children[0]) + }; + + res.push(( + NovelDelimiterLHS, + Vertex { + lhs_syntax: lhs_next, + rhs_syntax: v.rhs_syntax, + }, + )); + } + } + } + + if let Some(rhs_syntax) = &v.rhs_syntax { + match rhs_syntax { + // Step over this novel atom. + Syntax::Atom { .. } => { + res.push(( + NovelAtomRHS { same_line: false }, + Vertex { + lhs_syntax: v.lhs_syntax, + rhs_syntax: rhs_syntax.get_next(), + }, + )); + } + // Step into this partially/fully novel list. + Syntax::List { children, .. } => { + let rhs_next = if children.is_empty() { + rhs_syntax.get_next() + } else { + Some(children[0]) + }; + + res.push(( + NovelDelimiterRHS, + Vertex { + lhs_syntax: v.lhs_syntax, + rhs_syntax: rhs_next, + }, + )); + } + } + } + + res +} + +pub fn mark_syntax<'a>(lhs_syntax: Option<&'a Syntax<'a>>, rhs_syntax: Option<&'a Syntax<'a>>) { + let start = Vertex { + lhs_syntax, + rhs_syntax, + }; + let route = shortest_path(start); + mark_route(&route); +} + +fn mark_route(route: &[(Edge, Vertex)]) { + for (e, v) in route { + match e { + StartNode => { + // No change on the root node. + let lhs = v.lhs_syntax.unwrap(); + let rhs = v.rhs_syntax.unwrap(); + lhs.set_change(ChangeKind::Unchanged(rhs)); + rhs.set_change(ChangeKind::Unchanged(lhs)); + } + UnchangedNode => { + // No change on this node or its children. + let lhs = v.lhs_syntax.unwrap(); + let rhs = v.rhs_syntax.unwrap(); + lhs.set_change_deep(ChangeKind::Unchanged(rhs)); + rhs.set_change_deep(ChangeKind::Unchanged(lhs)); + } + UnchangedDelimiter => { + // No change on the outer delimiter, but children may + // have changed. + let lhs = v.lhs_syntax.unwrap(); + let rhs = v.rhs_syntax.unwrap(); + lhs.set_change(ChangeKind::Unchanged(rhs)); + rhs.set_change(ChangeKind::Unchanged(lhs)); + } + NovelAtomLHS { .. } | NovelDelimiterLHS => { + let lhs = v.lhs_syntax.unwrap(); + lhs.set_change(ChangeKind::Novel); + } + NovelAtomRHS { .. } | NovelDelimiterRHS => { + let rhs = v.rhs_syntax.unwrap(); + rhs.set_change(ChangeKind::Novel); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::positions::SingleLineSpan; + use crate::syntax::Syntax::*; + use crate::syntax::{set_next, AtomKind}; + + use itertools::Itertools; + use std::cell::Cell; + use typed_arena::Arena; + + fn pos_helper(line: usize) -> Vec { + vec![SingleLineSpan { + line: line.into(), + start_col: 0, + end_col: 1, + }] + } + + fn col_helper(line: usize, col: usize) -> Vec { + vec![SingleLineSpan { + line: line.into(), + start_col: col, + end_col: col + 1, + }] + } + + #[test] + fn identical_atoms() { + let arena = Arena::new(); + + let lhs = arena.alloc(Atom { + pos_content_hash: 0, + next: Cell::new(None), + position: pos_helper(0), + change: Cell::new(None), + content: "foo".into(), + kind: AtomKind::Other, + }); + + // Same content as LHS. + let rhs = arena.alloc(Atom { + pos_content_hash: 1, + next: Cell::new(None), + position: pos_helper(1), + change: Cell::new(None), + content: "foo".into(), + kind: AtomKind::Other, + }); + + let start = Vertex { + lhs_syntax: Some(lhs), + rhs_syntax: Some(rhs), + }; + let route = shortest_path(start); + + let actions = route.iter().map(|(action, _)| *action).collect_vec(); + assert_eq!(actions, vec![StartNode, UnchangedNode]); + } + + #[test] + fn extra_atom_lhs() { + let arena = Arena::new(); + + let lhs: Vec<&Syntax> = vec![Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![Syntax::new_atom( + &arena, + pos_helper(1), + "foo", + AtomKind::Other, + )], + "]".into(), + pos_helper(2), + )]; + set_next(&lhs, None); + + let rhs: Vec<&Syntax> = vec![Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![], + "]".into(), + pos_helper(2), + )]; + set_next(&rhs, None); + + let start = Vertex { + lhs_syntax: lhs.get(0).map(|n| *n), + rhs_syntax: rhs.get(0).map(|n| *n), + }; + let route = shortest_path(start); + + let actions = route.iter().map(|(action, _)| *action).collect_vec(); + assert_eq!( + actions, + vec![ + StartNode, + UnchangedDelimiter, + NovelAtomLHS { same_line: false } + ] + ); + } + + #[test] + fn repeated_atoms() { + let arena = Arena::new(); + + let lhs: Vec<&Syntax> = vec![Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![], + "]".into(), + pos_helper(2), + )]; + set_next(&lhs, None); + + let rhs: Vec<&Syntax> = vec![Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![ + Syntax::new_atom(&arena, pos_helper(1), "foo", AtomKind::Other), + Syntax::new_atom(&arena, pos_helper(2), "foo", AtomKind::Other), + ], + "]".into(), + pos_helper(3), + )]; + set_next(&rhs, None); + + let start = Vertex { + lhs_syntax: lhs.get(0).map(|n| *n), + rhs_syntax: rhs.get(0).map(|n| *n), + }; + let route = shortest_path(start); + + let actions = route.iter().map(|(action, _)| *action).collect_vec(); + assert_eq!( + actions, + vec![ + StartNode, + UnchangedDelimiter, + NovelAtomRHS { same_line: false }, + NovelAtomRHS { same_line: false } + ] + ); + } + + #[test] + fn atom_after_empty_list() { + let arena = Arena::new(); + + let lhs: Vec<&Syntax> = vec![Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![ + Syntax::new_list( + &arena, + "(".into(), + pos_helper(1), + vec![], + ")".into(), + pos_helper(2), + ), + Syntax::new_atom(&arena, pos_helper(3), "foo", AtomKind::Other), + ], + "]".into(), + pos_helper(4), + )]; + set_next(&lhs, None); + + let rhs: Vec<&Syntax> = vec![Syntax::new_list( + &arena, + "{".into(), + pos_helper(0), + vec![ + Syntax::new_list( + &arena, + "(".into(), + pos_helper(1), + vec![], + ")".into(), + pos_helper(2), + ), + Syntax::new_atom(&arena, pos_helper(3), "foo", AtomKind::Other), + ], + "}".into(), + pos_helper(4), + )]; + set_next(&rhs, None); + + let start = Vertex { + lhs_syntax: lhs.get(0).map(|n| *n), + rhs_syntax: rhs.get(0).map(|n| *n), + }; + let route = shortest_path(start); + + let actions = route.iter().map(|(action, _)| *action).collect_vec(); + assert_eq!( + actions, + vec![ + StartNode, + NovelDelimiterLHS, + NovelDelimiterRHS, + UnchangedNode, + UnchangedNode + ], + ); + } + #[test] + fn prefer_atoms_same_line() { + let arena = Arena::new(); + + let lhs: Vec<&Syntax> = vec![ + Syntax::new_atom(&arena, col_helper(1, 0), "foo", AtomKind::Other), + Syntax::new_atom(&arena, col_helper(2, 0), "bar", AtomKind::Other), + Syntax::new_atom(&arena, col_helper(2, 1), "foo", AtomKind::Other), + ]; + set_next(&lhs, None); + + let rhs: Vec<&Syntax> = vec![Syntax::new_atom( + &arena, + col_helper(1, 0), + "foo", + AtomKind::Other, + )]; + set_next(&rhs, None); + + let start = Vertex { + lhs_syntax: lhs.get(0).map(|n| *n), + rhs_syntax: rhs.get(0).map(|n| *n), + }; + let route = shortest_path(start); + + let actions = route.iter().map(|(action, _)| *action).collect_vec(); + assert_eq!( + actions, + vec![ + StartNode, + NovelAtomLHS { same_line: true }, + NovelAtomLHS { same_line: false }, + UnchangedNode, + ], + ); + } +} diff --git a/sample_files/slow_before.rs b/sample_files/slow_before.rs new file mode 100644 index 000000000..220840586 --- /dev/null +++ b/sample_files/slow_before.rs @@ -0,0 +1,571 @@ +use std::cmp::Ordering; +use std::collections::BinaryHeap; + +use crate::syntax::{ChangeKind, Syntax}; +use rustc_hash::{FxHashMap, FxHashSet}; +use typed_arena::Arena; +use Edge::*; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct Vertex<'a> { + lhs_syntax: Option<&'a Syntax<'a>>, + rhs_syntax: Option<&'a Syntax<'a>>, +} + +impl<'a> Vertex<'a> { + fn new(lhs: &'a Syntax<'a>, rhs: &'a Syntax<'a>) -> Self { + Self { + lhs_syntax: Some(lhs), + rhs_syntax: Some(rhs), + } + } + + fn is_end(&self) -> bool { + self.lhs_syntax.is_none() && self.rhs_syntax.is_none() + } +} + +// Rust requires that PartialEq, PartialOrd and Ord agree. +// https://doc.rust-lang.org/std/cmp/trait.Ord.html +// +// For `Vertex`, we want to compare by distance in a priority queue, but +// equality should only consider LHS/RHS node when deciding if we've +// visited a vertex. We define separate wrappers for these two use +// cases. +#[derive(Debug)] +struct OrdVertex<'a> { + distance: i64, + v: Vertex<'a>, +} + +impl<'a> PartialOrd for OrdVertex<'a> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl<'a> Ord for OrdVertex<'a> { + fn cmp(&self, other: &Self) -> Ordering { + self.distance.cmp(&other.distance) + } +} + +impl<'a> PartialEq for OrdVertex<'a> { + fn eq(&self, other: &Self) -> bool { + self.distance == other.distance + } +} +impl<'a> Eq for OrdVertex<'a> {} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +enum Edge { + StartNode, + UnchangedNode, + UnchangedDelimiter, + NovelAtomLHS { same_line: bool }, + NovelAtomRHS { same_line: bool }, + NovelDelimiterLHS, + NovelDelimiterRHS, +} + +impl Edge { + fn cost(&self) -> i64 { + match self { + StartNode => 0, + // Matching nodes is always best. + UnchangedNode => 0, + // Matching an outer delimiter is good. + UnchangedDelimiter => -1, + // Otherwise, we've added/removed a node. Prefer novel + // nodes on the same line, to reduce the risk of sliders. + NovelAtomLHS { same_line } | NovelAtomRHS { same_line } => { + if *same_line { + -2 + } else { + -3 + } + } + NovelDelimiterLHS => -2, + NovelDelimiterRHS => -2, + } + } +} + +fn shortest_path(start: Vertex) -> Vec<(Edge, Vertex)> { + let mut heap = BinaryHeap::new(); + heap.push(OrdVertex { + distance: 0, + v: start.clone(), + }); + + let mut visited = FxHashSet::default(); + let mut predecessors: FxHashMap = FxHashMap::default(); + + loop { + match heap.pop() { + Some(OrdVertex { distance, v }) => { + if v.is_end() { + break; + } + + if visited.contains(&v) { + continue; + } + for (edge, new_v) in neighbours(&v) { + if !predecessors.contains_key(&new_v) { + predecessors.insert(new_v.clone(), (edge, v.clone())); + heap.push(OrdVertex { + distance: distance + edge.cost(), + v: new_v, + }); + } + } + + visited.insert(v); + } + None => panic!("Ran out of graph nodes before reaching end"), + } + } + + let mut current = Vertex { + lhs_syntax: None, + rhs_syntax: None, + }; + let mut res: Vec<(Edge, Vertex)> = vec![]; + loop { + match predecessors.remove(¤t) { + Some((edge, node)) => { + res.push((edge, node.clone())); + current = node; + } + None => { + res.push((StartNode, current.clone())); + break; + } + } + } + + res.reverse(); + res +} + +fn neighbours<'a>(v: &Vertex<'a>) -> Vec<(Edge, Vertex<'a>)> { + let mut res = vec![]; + + if let (Some(lhs_syntax), Some(rhs_syntax)) = (&v.lhs_syntax, &v.rhs_syntax) { + if lhs_syntax.equal_content(rhs_syntax) { + // Both nodes are equal, the happy case. + res.push(( + UnchangedNode, + Vertex { + lhs_syntax: lhs_syntax.get_next(), + rhs_syntax: rhs_syntax.get_next(), + }, + )); + } + + if let ( + Syntax::List { + open_content: lhs_open_content, + close_content: lhs_close_content, + children: lhs_children, + .. + }, + Syntax::List { + open_content: rhs_open_content, + close_content: rhs_close_content, + children: rhs_children, + .. + }, + ) = (lhs_syntax, rhs_syntax) + { + if lhs_open_content == rhs_open_content && lhs_close_content == rhs_close_content { + let lhs_next = if lhs_children.is_empty() { + lhs_syntax.get_next() + } else { + Some(lhs_children[0]) + }; + let rhs_next = if rhs_children.is_empty() { + rhs_syntax.get_next() + } else { + Some(rhs_children[0]) + }; + res.push(( + UnchangedDelimiter, + Vertex { + lhs_syntax: lhs_next, + rhs_syntax: rhs_next, + }, + )); + } + } + } + + if let Some(lhs_syntax) = &v.lhs_syntax { + match lhs_syntax { + // Step over this novel atom. + Syntax::Atom { .. } => { + res.push(( + NovelAtomLHS { same_line: false }, + Vertex { + lhs_syntax: lhs_syntax.get_next(), + rhs_syntax: v.rhs_syntax, + }, + )); + } + // Step into this partially/fully novel list. + Syntax::List { children, .. } => { + let lhs_next = if children.is_empty() { + lhs_syntax.get_next() + } else { + Some(children[0]) + }; + + res.push(( + NovelDelimiterLHS, + Vertex { + lhs_syntax: lhs_next, + rhs_syntax: v.rhs_syntax, + }, + )); + } + } + } + + if let Some(rhs_syntax) = &v.rhs_syntax { + match rhs_syntax { + // Step over this novel atom. + Syntax::Atom { .. } => { + res.push(( + NovelAtomRHS { same_line: false }, + Vertex { + lhs_syntax: v.lhs_syntax, + rhs_syntax: rhs_syntax.get_next(), + }, + )); + } + // Step into this partially/fully novel list. + Syntax::List { children, .. } => { + let rhs_next = if children.is_empty() { + rhs_syntax.get_next() + } else { + Some(children[0]) + }; + + res.push(( + NovelDelimiterRHS, + Vertex { + lhs_syntax: v.lhs_syntax, + rhs_syntax: rhs_next, + }, + )); + } + } + } + + res +} + +pub fn toplevel_list<'a>( + arena: &'a Arena>, + lhs_children: Vec<&'a Syntax<'a>>, + rhs_children: Vec<&'a Syntax<'a>>, +) -> (&'a Syntax<'a>, &'a Syntax<'a>) { + let lhs = Syntax::new_list(arena, "", vec![], lhs_children, "", vec![]); + let rhs = Syntax::new_list(arena, "", vec![], rhs_children, "", vec![]); + (lhs, rhs) +} + +pub fn mark_syntax<'a>(lhs: &'a Syntax<'a>, rhs: &'a Syntax<'a>) { + let start = Vertex::new(lhs, rhs); + let route = shortest_path(start); + mark_route(&route); +} + +fn mark_route(route: &[(Edge, Vertex)]) { + for (e, v) in route { + match e { + StartNode => { + // No change on the root node. + let lhs = v.lhs_syntax.unwrap(); + let rhs = v.rhs_syntax.unwrap(); + lhs.set_change(ChangeKind::Unchanged(rhs)); + rhs.set_change(ChangeKind::Unchanged(lhs)); + } + UnchangedNode => { + // No change on this node or its children. + let lhs = v.lhs_syntax.unwrap(); + let rhs = v.rhs_syntax.unwrap(); + lhs.set_change_deep(ChangeKind::Unchanged(rhs)); + rhs.set_change_deep(ChangeKind::Unchanged(lhs)); + } + UnchangedDelimiter => { + // No change on the outer delimiter, but children may + // have changed. + let lhs = v.lhs_syntax.unwrap(); + let rhs = v.rhs_syntax.unwrap(); + lhs.set_change(ChangeKind::Unchanged(rhs)); + rhs.set_change(ChangeKind::Unchanged(lhs)); + } + NovelAtomLHS { .. } | NovelDelimiterLHS => { + let lhs = v.lhs_syntax.unwrap(); + lhs.set_change(ChangeKind::Novel); + } + NovelAtomRHS { .. } | NovelDelimiterRHS => { + let rhs = v.rhs_syntax.unwrap(); + rhs.set_change(ChangeKind::Novel); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::positions::SingleLineSpan; + use crate::syntax::Syntax::*; + use crate::syntax::{set_next, AtomKind}; + + use itertools::Itertools; + use std::cell::Cell; + use typed_arena::Arena; + + fn pos_helper(line: usize) -> Vec { + vec![SingleLineSpan { + line: line.into(), + start_col: 0, + end_col: 1, + }] + } + + fn col_helper(line: usize, col: usize) -> Vec { + vec![SingleLineSpan { + line: line.into(), + start_col: col, + end_col: col + 1, + }] + } + + #[test] + fn identical_atoms() { + let arena = Arena::new(); + + let lhs = arena.alloc(Atom { + pos_content_hash: 0, + next: Cell::new(None), + position: pos_helper(0), + change: Cell::new(None), + content: "foo".into(), + kind: AtomKind::Other, + }); + + // Same content as LHS. + let rhs = arena.alloc(Atom { + pos_content_hash: 1, + next: Cell::new(None), + position: pos_helper(1), + change: Cell::new(None), + content: "foo".into(), + kind: AtomKind::Other, + }); + + let start = Vertex::new(lhs, rhs); + let route = shortest_path(start); + + let actions = route.iter().map(|(action, _)| *action).collect_vec(); + assert_eq!(actions, vec![StartNode, UnchangedNode]); + } + + #[test] + fn extra_atom_lhs() { + let arena = Arena::new(); + + let lhs = Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![Syntax::new_atom( + &arena, + pos_helper(1), + "foo", + AtomKind::Other, + )], + "]".into(), + pos_helper(2), + ); + set_next(lhs); + + let rhs = Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![], + "]".into(), + pos_helper(1), + ); + set_next(rhs); + + let start = Vertex::new(lhs, rhs); + let route = shortest_path(start); + + let actions = route.iter().map(|(action, _)| *action).collect_vec(); + assert_eq!( + actions, + vec![ + StartNode, + UnchangedDelimiter, + NovelAtomLHS { same_line: false } + ] + ); + } + + #[test] + fn repeated_atoms() { + let arena = Arena::new(); + + let lhs = Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![], + "]".into(), + pos_helper(2), + ); + set_next(lhs); + + let rhs = Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![ + Syntax::new_atom(&arena, pos_helper(1), "foo", AtomKind::Other), + Syntax::new_atom(&arena, pos_helper(2), "foo", AtomKind::Other), + ], + "]".into(), + pos_helper(3), + ); + set_next(rhs); + + let start = Vertex::new(lhs, rhs); + let route = shortest_path(start); + + let actions = route.iter().map(|(action, _)| *action).collect_vec(); + assert_eq!( + actions, + vec![ + StartNode, + UnchangedDelimiter, + NovelAtomRHS { same_line: false }, + NovelAtomRHS { same_line: false } + ] + ); + } + + #[test] + fn atom_after_empty_list() { + let arena = Arena::new(); + + let lhs = Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![ + Syntax::new_list( + &arena, + "(".into(), + pos_helper(1), + vec![], + ")".into(), + pos_helper(2), + ), + Syntax::new_atom(&arena, pos_helper(3), "foo", AtomKind::Other), + ], + "]".into(), + pos_helper(4), + ); + set_next(lhs); + + let rhs = Syntax::new_list( + &arena, + "{".into(), + pos_helper(0), + vec![ + Syntax::new_list( + &arena, + "(".into(), + pos_helper(1), + vec![], + ")".into(), + pos_helper(2), + ), + Syntax::new_atom(&arena, pos_helper(3), "foo", AtomKind::Other), + ], + "}".into(), + pos_helper(4), + ); + set_next(rhs); + + let start = Vertex::new(lhs, rhs); + let route = shortest_path(start); + + let actions = route.iter().map(|(action, _)| *action).collect_vec(); + assert_eq!( + actions, + vec![ + StartNode, + NovelDelimiterLHS, + NovelDelimiterRHS, + UnchangedNode, + UnchangedNode + ], + ); + } + #[test] + fn prefer_atoms_same_line() { + let arena = Arena::new(); + + let lhs = Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![ + Syntax::new_atom(&arena, col_helper(1, 0), "foo", AtomKind::Other), + Syntax::new_atom(&arena, col_helper(2, 0), "bar", AtomKind::Other), + Syntax::new_atom(&arena, col_helper(2, 1), "foo", AtomKind::Other), + ], + "]".into(), + pos_helper(4), + ); + set_next(lhs); + + let rhs = Syntax::new_list( + &arena, + "[".into(), + pos_helper(0), + vec![Syntax::new_atom( + &arena, + col_helper(1, 0), + "foo", + AtomKind::Other, + )], + "]".into(), + pos_helper(4), + ); + set_next(rhs); + + let start = Vertex::new(lhs, rhs); + let route = shortest_path(start); + + let actions = route.iter().map(|(action, _)| *action).collect_vec(); + assert_eq!( + actions, + vec![ + StartNode, + UnchangedDelimiter, + NovelAtomLHS { same_line: false }, + NovelAtomLHS { same_line: false }, + UnchangedNode, + ], + ); + } +}