diff --git a/crates/no-mistakes/src/edge_index/adjacency.rs b/crates/no-mistakes/src/edge_index/adjacency.rs index 29ec9ed26..7a7fc2d5a 100644 --- a/crates/no-mistakes/src/edge_index/adjacency.rs +++ b/crates/no-mistakes/src/edge_index/adjacency.rs @@ -144,6 +144,24 @@ pub(crate) fn push_ordinal( } } +pub(crate) fn remember_pair( + known: &mut FxHashMap>, + to: &Node, + kind: &Kind, +) -> bool +where + Node: Clone + Eq + Hash, + Kind: Clone + Eq + Hash, +{ + if let Some(kinds) = known.get_mut(to) { + return kinds.insert(kind.clone()); + } + let mut kinds = FxHashSet::default(); + kinds.insert(kind.clone()); + known.insert(to.clone(), kinds); + true +} + pub(crate) fn seed_known_targets( existing: Option<&Adjacency>, ) -> FxHashMap> @@ -151,17 +169,17 @@ where Node: Clone + Eq + Hash, Kind: Clone + Eq + Hash, { - match existing { - Some(adj) => { - let mut known: FxHashMap> = - fx_map_with_capacity(adj.neighbors.len()); - for (to, kind) in &adj.neighbors { - known.entry(to.clone()).or_default().insert(kind.clone()); - } - known - } - None => FxHashMap::default(), + let Some(adj) = existing else { + return FxHashMap::default(); + }; + if adj.neighbors.is_empty() { + return FxHashMap::default(); + } + let mut known: FxHashMap> = fx_map_with_capacity(adj.neighbors.len()); + for (to, kind) in &adj.neighbors { + remember_pair(&mut known, to, kind); } + known } #[cfg(test)] diff --git a/crates/no-mistakes/src/edge_index/adjacency/tests.rs b/crates/no-mistakes/src/edge_index/adjacency/tests.rs index f2a18e378..05952c096 100644 --- a/crates/no-mistakes/src/edge_index/adjacency/tests.rs +++ b/crates/no-mistakes/src/edge_index/adjacency/tests.rs @@ -1,4 +1,6 @@ -use super::{into_adjacency_map, push_neighbor, push_ordinal, seed_known_targets, Adjacency}; +use super::{ + into_adjacency_map, push_neighbor, push_ordinal, remember_pair, seed_known_targets, Adjacency, +}; use crate::fx::{fx_map, FxHashMap}; fn one_adj(key: &str, adj: Adjacency) -> FxHashMap> { @@ -88,14 +90,22 @@ fn push_ordinal_hit_and_miss_do_not_clone_on_hit() { fn seed_known_targets_covers_existing_and_missing_sources() { for (existing, expected_len) in [ (None, 0), + ( + Some(Adjacency { + neighbors: Vec::new(), + ordinals: Vec::new(), + }), + 0, + ), ( Some(Adjacency { neighbors: vec![ ("b".to_owned(), 1_u8), + ("b".to_owned(), 1), ("b".to_owned(), 2), ("c".to_owned(), 1), ], - ordinals: vec![0, 1, 2], + ordinals: vec![0, 1, 2, 3], }), 2, ), @@ -111,6 +121,19 @@ fn seed_known_targets_covers_existing_and_missing_sources() { } } +#[test] +fn remember_pair_inserts_once_per_target_kind() { + let mut known = fx_map(); + assert!(remember_pair(&mut known, &"b".to_owned(), &1_u8)); + assert!(remember_pair(&mut known, &"b".to_owned(), &2)); + assert!(!remember_pair(&mut known, &"b".to_owned(), &1)); + assert!(remember_pair(&mut known, &"c".to_owned(), &1)); + assert_eq!(known.len(), 2); + assert!(known + .get("b") + .is_some_and(|kinds| kinds.contains(&1) && kinds.contains(&2))); +} + #[test] fn into_adjacency_map_preserves_keys_and_presizes_ordinals() { let mut input = fx_map(); diff --git a/crates/no-mistakes/src/edge_index/build/extend.rs b/crates/no-mistakes/src/edge_index/build/extend.rs index 77abddf99..f7471fb5d 100644 --- a/crates/no-mistakes/src/edge_index/build/extend.rs +++ b/crates/no-mistakes/src/edge_index/build/extend.rs @@ -1,4 +1,4 @@ -use super::super::adjacency::{push_neighbor, seed_known_targets}; +use super::super::adjacency::{push_neighbor, remember_pair, seed_known_targets, Adjacency}; use super::super::{CanonicalEdge, EdgeIndex}; use crate::fx::{fx_map_with_capacity, FxHashMap, FxHashSet}; use std::hash::Hash; @@ -21,48 +21,49 @@ where let (lower, upper) = edges.size_hint(); let mut known_by_source = fx_map_with_capacity(upper.unwrap_or(lower)); for edge in edges { - if !known_by_source.contains_key(&edge.from) { - known_by_source.insert( - edge.from.clone(), - seed_known_targets(self.forward.get(&edge.from)), - ); - } - if pair_is_known(&known_by_source[&edge.from], &edge.to, &edge.kind) { + if !record_unknown_pair( + &mut known_by_source, + &self.forward, + &edge.from, + &edge.to, + &edge.kind, + ) { continue; } - let known = known_by_source - .get_mut(&edge.from) - .expect("source known-set is inserted before accept"); - known - .entry(edge.to.clone()) - .or_default() - .insert(edge.kind.clone()); let ordinal = self.edges.len(); + let CanonicalEdge { from, to, kind } = edge; push_neighbor( &mut self.forward, - &edge.from, - (edge.to.clone(), edge.kind.clone()), + &from, + (to.clone(), kind.clone()), ordinal, ); push_neighbor( &mut self.reverse, - &edge.to, - (edge.from.clone(), edge.kind.clone()), + &to, + (from.clone(), kind.clone()), ordinal, ); - self.edges.push(edge); + self.edges.push(CanonicalEdge { from, to, kind }); } } } -fn pair_is_known( - known: &FxHashMap>, +fn record_unknown_pair( + known_by_source: &mut FxHashMap>>, + forward: &FxHashMap>, + from: &Node, to: &Node, kind: &Kind, ) -> bool where - Node: Eq + Hash, - Kind: Eq + Hash, + Node: Clone + Eq + Hash, + Kind: Clone + Eq + Hash, { - known.get(to).is_some_and(|kinds| kinds.contains(kind)) + if let Some(known) = known_by_source.get_mut(from) { + return remember_pair(known, to, kind); + } + let seeded = seed_known_targets(forward.get(from)); + let known = known_by_source.entry(from.clone()).or_insert(seeded); + remember_pair(known, to, kind) } diff --git a/crates/no-mistakes/src/edge_index/tests_extend.rs b/crates/no-mistakes/src/edge_index/tests_extend.rs index 46aec4a1d..e550580f4 100644 --- a/crates/no-mistakes/src/edge_index/tests_extend.rs +++ b/crates/no-mistakes/src/edge_index/tests_extend.rs @@ -85,16 +85,35 @@ fn extend_edges_preserving_ordinals_matches_rebuild_cases() { fn extend_edges_from_seeded_empty_source_hits_existing_adjacency() { let mut index = EdgeIndex::from_edges_and_nodes(std::iter::empty(), ["a".to_owned(), "orphan".to_owned()]); - index.extend_edges_preserving_ordinals([edge("a", "b", 1), edge("a", "b", 1)]); - assert_eq!(index.edges(), &[edge("a", "b", 1)]); + index.extend_edges_preserving_ordinals([ + edge("a", "b", 1), + edge("a", "b", 1), + edge("a", "b", 2), + edge("a", "c", 3), + edge("z", "b", 4), + edge("z", "b", 4), + ]); + assert_eq!( + index.edges(), + &[ + edge("a", "b", 1), + edge("a", "b", 2), + edge("a", "c", 3), + edge("z", "b", 4) + ] + ); assert_eq!( index.forward().get("a").map(|adj| adj.ordinals.as_slice()), - Some(&[0][..]) + Some(&[0, 1, 2][..]) ); assert!(index .forward() .get("orphan") .is_some_and(|adj| adj.neighbors.is_empty())); + assert_eq!( + index.reverse().get("b").map(|adj| adj.ordinals.as_slice()), + Some(&[0, 1, 3][..]) + ); } #[test]