1#![forbid(unsafe_code)]
2
3pub mod digraph;
4
5use fnx_runtime::{
6 CgseValue, CompatibilityMode, DecisionAction, DecisionRecord, EvidenceLedger, EvidenceTerm,
7 RuntimePolicy, bool_evidence, count_evidence,
8};
9use indexmap::{
10 IndexMap,
11 map::{RawEntryApiV1, raw_entry_v1::RawEntryMut},
12};
13use serde::{Deserialize, Serialize};
14use std::collections::BTreeMap;
15use std::hash::BuildHasher;
16
17pub(crate) type FxIndexMap<K, V> = IndexMap<K, V, rustc_hash::FxBuildHasher>;
24pub(crate) type FxIndexSet<T> = indexmap::IndexSet<T, rustc_hash::FxBuildHasher>;
30use std::collections::HashSet;
31use std::fmt;
32
33pub type AttrMap = BTreeMap<String, CgseValue>;
34
35#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum GraphError {
37 FailClosed {
38 operation: &'static str,
39 reason: String,
40 },
41}
42
43impl fmt::Display for GraphError {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 Self::FailClosed { operation, reason } => {
47 write!(f, "operation `{operation}` failed closed: {reason}")
48 }
49 }
50 }
51}
52
53impl std::error::Error for GraphError {}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct EdgeSnapshot {
57 pub left: String,
58 pub right: String,
59 pub attrs: AttrMap,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct GraphSnapshot {
64 pub mode: CompatibilityMode,
65 pub nodes: Vec<String>,
66 #[serde(default)]
72 pub node_attrs: BTreeMap<String, AttrMap>,
73 pub edges: Vec<EdgeSnapshot>,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct MultiEdgeSnapshot {
78 pub left: String,
79 pub right: String,
80 pub key: usize,
81 pub attrs: AttrMap,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct MultiGraphSnapshot {
86 pub mode: CompatibilityMode,
87 pub nodes: Vec<String>,
88 #[serde(default)]
90 pub node_attrs: BTreeMap<String, AttrMap>,
91 pub edges: Vec<MultiEdgeSnapshot>,
92}
93
94#[derive(Debug)]
95pub struct Graph {
96 mode: CompatibilityMode,
97 revision: u64,
98 nodes: FxIndexMap<String, AttrMap>,
99 adj_indices: Vec<Vec<usize>>,
106 all_int_cache: std::sync::Arc<std::sync::RwLock<Option<(u64, String, bool)>>>,
109 edge_slots_by_neighbor: std::sync::RwLock<Option<(u64, Vec<Vec<usize>>)>>,
115 edge_index_endpoints: Vec<(usize, usize)>,
116 edges: FxIndexMap<(usize, usize), AttrMap>,
122 runtime_policy: RuntimePolicy,
123}
124
125impl Clone for Graph {
126 fn clone(&self) -> Self {
127 Self {
128 mode: self.mode,
129 revision: self.revision,
130 nodes: self.nodes.clone(),
131 adj_indices: self.adj_indices.clone(),
132 all_int_cache: std::sync::Arc::default(),
136 edge_slots_by_neighbor: std::sync::RwLock::default(),
137 edge_index_endpoints: self.edge_index_endpoints.clone(),
138 edges: self.edges.clone(),
139 runtime_policy: self.runtime_policy.clone(),
140 }
141 }
142}
143
144impl Graph {
145 #[must_use]
146 pub fn new(mode: CompatibilityMode) -> Self {
147 Self {
148 mode,
149 revision: 0,
150 nodes: FxIndexMap::default(),
151 adj_indices: Vec::new(),
152 all_int_cache: std::sync::Arc::default(),
153 edge_slots_by_neighbor: std::sync::RwLock::default(),
154 edge_index_endpoints: Vec::new(),
155 edges: FxIndexMap::default(),
156 runtime_policy: RuntimePolicy::new(mode),
157 }
158 }
159
160 #[must_use]
161 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
162 let mode = runtime_policy.mode();
163 Self {
164 mode,
165 revision: 0,
166 nodes: FxIndexMap::default(),
167 adj_indices: Vec::new(),
168 all_int_cache: std::sync::Arc::default(),
169 edge_slots_by_neighbor: std::sync::RwLock::default(),
170 edge_index_endpoints: Vec::new(),
171 edges: FxIndexMap::default(),
172 runtime_policy,
173 }
174 }
175
176 pub fn clear_edges(&mut self) {
183 if self.edges.is_empty() && self.edge_index_endpoints.is_empty() {
184 return;
185 }
186 self.edges.clear();
187 self.edge_index_endpoints.clear();
188 for row in &mut self.adj_indices {
189 row.clear();
190 }
191 self.revision = self.revision.saturating_add(1);
192 }
193
194 #[must_use]
195 pub fn strict() -> Self {
196 Self::new(CompatibilityMode::Strict)
197 }
198
199 #[must_use]
200 pub fn complete_graph(mode: CompatibilityMode, n: usize) -> Self {
201 let edge_capacity = n.saturating_mul(n.saturating_sub(1)) / 2;
202 let revision = u64::try_from(n)
203 .unwrap_or(u64::MAX)
204 .saturating_add(u64::try_from(edge_capacity).unwrap_or(u64::MAX));
205 let node_labels = (0..n).map(|node| node.to_string()).collect::<Vec<_>>();
206 let mut graph = Self {
207 mode,
208 revision,
209 nodes: FxIndexMap::with_capacity_and_hasher(n, rustc_hash::FxBuildHasher),
210 adj_indices: vec![Vec::with_capacity(n.saturating_sub(1)); n],
211 all_int_cache: std::sync::Arc::default(),
212 edge_slots_by_neighbor: std::sync::RwLock::default(),
213 edge_index_endpoints: Vec::with_capacity(edge_capacity),
214 edges: FxIndexMap::with_capacity_and_hasher(edge_capacity, rustc_hash::FxBuildHasher),
215 runtime_policy: RuntimePolicy::new(mode),
216 };
217
218 for node in &node_labels {
219 graph.nodes.insert(node.clone(), AttrMap::new());
220 }
221
222 for left_index in 0..n {
223 let _left = &node_labels[left_index];
224 for (right_index, _right) in node_labels.iter().enumerate().skip(left_index + 1) {
225 graph.adj_indices[left_index].push(right_index);
227 graph.adj_indices[right_index].push(left_index);
228 graph.edge_index_endpoints.push((left_index, right_index));
229 graph
230 .edges
231 .insert(Graph::canon_pair(left_index, right_index), AttrMap::new());
232 }
233 }
234
235 graph.record_decision(
236 "complete_graph_bulk",
237 0.0,
238 false,
239 vec![EvidenceTerm {
240 signal: "nodes".into(),
241 observed_value: count_evidence(n),
242 log_likelihood_ratio: 0.0,
243 }],
244 );
245 graph
246 }
247
248 #[must_use]
252 pub fn grid_2d(mode: CompatibilityMode, m: usize, n: usize) -> Self {
253 let node_count = m.saturating_mul(n);
254 let edge_capacity = m
255 .saturating_sub(1)
256 .saturating_mul(n)
257 .saturating_add(n.saturating_sub(1).saturating_mul(m));
258 let revision = u64::try_from(node_count)
259 .unwrap_or(u64::MAX)
260 .saturating_add(u64::try_from(edge_capacity).unwrap_or(u64::MAX));
261 let label = |i: usize, j: usize| format!("({i}, {j})");
262 let idx = |i: usize, j: usize| i * n + j;
263 let mut graph = Self {
264 mode,
265 revision,
266 nodes: FxIndexMap::with_capacity_and_hasher(node_count, rustc_hash::FxBuildHasher),
267 adj_indices: vec![Vec::with_capacity(4); node_count],
268 all_int_cache: std::sync::Arc::default(),
269 edge_slots_by_neighbor: std::sync::RwLock::default(),
270 edge_index_endpoints: Vec::with_capacity(edge_capacity),
271 edges: FxIndexMap::with_capacity_and_hasher(edge_capacity, rustc_hash::FxBuildHasher),
272 runtime_policy: RuntimePolicy::new(mode),
273 };
274 for i in 0..m {
275 for j in 0..n {
276 let key = label(i, j);
277 graph.nodes.insert(key.clone(), AttrMap::new());
278 }
279 }
280 fn add_grid_edge(graph: &mut Graph, _u: String, _v: String, u_idx: usize, v_idx: usize) {
281 graph.adj_indices[u_idx].push(v_idx);
283 graph.adj_indices[v_idx].push(u_idx);
284 graph.edge_index_endpoints.push((u_idx, v_idx));
285 graph
286 .edges
287 .insert(Graph::canon_pair(u_idx, v_idx), AttrMap::new());
288 }
289 for i in 1..m {
291 for j in 0..n {
292 add_grid_edge(
293 &mut graph,
294 label(i, j),
295 label(i - 1, j),
296 idx(i, j),
297 idx(i - 1, j),
298 );
299 }
300 }
301 for i in 0..m {
303 for j in 1..n {
304 add_grid_edge(
305 &mut graph,
306 label(i, j),
307 label(i, j - 1),
308 idx(i, j),
309 idx(i, j - 1),
310 );
311 }
312 }
313 graph.record_decision(
314 "grid_2d_bulk",
315 0.0,
316 false,
317 vec![EvidenceTerm {
318 signal: "nodes".into(),
319 observed_value: count_evidence(node_count),
320 log_likelihood_ratio: 0.0,
321 }],
322 );
323 graph
324 }
325
326 pub fn grid_nd(
332 mode: CompatibilityMode,
333 dimensions: &[usize],
334 periodic: &[bool],
335 ) -> Result<Self, GraphError> {
336 if dimensions.len() != periodic.len() {
337 return Err(GraphError::FailClosed {
338 operation: "grid_nd",
339 reason: format!(
340 "periodic flag count {} does not match dimension count {}",
341 periodic.len(),
342 dimensions.len()
343 ),
344 });
345 }
346 if dimensions.is_empty() {
347 return Ok(Self::new(mode));
348 }
349
350 #[derive(Clone)]
351 struct ProductState {
352 node_count: usize,
353 adjacency: Vec<Vec<usize>>,
354 edge_set: HashSet<(usize, usize)>,
355 }
356
357 impl ProductState {
358 fn empty() -> Self {
359 Self {
360 node_count: 0,
361 adjacency: Vec::new(),
362 edge_set: HashSet::new(),
363 }
364 }
365
366 fn axis(size: usize, periodic: bool) -> Self {
367 let mut state = Self {
368 node_count: size,
369 adjacency: vec![Vec::new(); size],
370 edge_set: HashSet::new(),
371 };
372 if size == 0 {
373 return state;
374 }
375 if periodic {
376 if size == 1 {
377 state.add_edge(0, 0);
378 } else {
379 for node in 0..(size - 1) {
380 state.add_edge(node, node + 1);
381 }
382 state.add_edge(size - 1, 0);
383 }
384 } else {
385 for node in 0..size.saturating_sub(1) {
386 state.add_edge(node, node + 1);
387 }
388 }
389 state
390 }
391
392 fn product(axis: &Self, old: &Self) -> Result<Self, GraphError> {
393 let Some(node_count) = axis.node_count.checked_mul(old.node_count) else {
394 return Err(GraphError::FailClosed {
395 operation: "grid_nd",
396 reason: "node count overflow during cartesian product".to_owned(),
397 });
398 };
399 if node_count == 0 {
400 return Ok(Self::empty());
401 }
402
403 let axis_edges = axis.edges_view();
404 let old_edges = old.edges_view();
405 let mut state = Self {
406 node_count,
407 adjacency: vec![Vec::new(); node_count],
408 edge_set: HashSet::with_capacity(
409 axis_edges
410 .len()
411 .saturating_mul(old.node_count)
412 .saturating_add(old_edges.len().saturating_mul(axis.node_count)),
413 ),
414 };
415
416 for (axis_left, axis_right) in axis_edges {
417 let left_base = axis_left * old.node_count;
418 let right_base = axis_right * old.node_count;
419 for old_index in 0..old.node_count {
420 state.add_edge(left_base + old_index, right_base + old_index);
421 }
422 }
423 for axis_index in 0..axis.node_count {
424 let base = axis_index * old.node_count;
425 for &(old_left, old_right) in &old_edges {
426 state.add_edge(base + old_left, base + old_right);
427 }
428 }
429 Ok(state)
430 }
431
432 fn add_edge(&mut self, left: usize, right: usize) {
433 let key = if left <= right {
434 (left, right)
435 } else {
436 (right, left)
437 };
438 if !self.edge_set.insert(key) {
439 return;
440 }
441 self.adjacency[left].push(right);
442 if left != right {
443 self.adjacency[right].push(left);
444 }
445 }
446
447 fn edges_view(&self) -> Vec<(usize, usize)> {
448 let mut edges = Vec::with_capacity(self.edge_set.len());
449 let mut seen = vec![false; self.node_count];
450 for left in 0..self.node_count {
451 for &right in &self.adjacency[left] {
452 if !seen[right] {
453 edges.push((left, right));
454 }
455 }
456 seen[left] = true;
457 }
458 edges
459 }
460 }
461
462 fn coords_from_index(mut index: usize, sizes: &[usize]) -> Vec<usize> {
463 let mut coords = vec![0; sizes.len()];
464 for axis in (0..sizes.len()).rev() {
465 let size = sizes[axis];
466 coords[axis] = index % size;
467 index /= size;
468 }
469 coords
470 }
471
472 fn tuple_canonical(coords: &[usize]) -> String {
473 if let [value] = coords {
474 return value.to_string();
475 }
476 let mut s = String::with_capacity(coords.len() * 6 + 2);
477 s.push('(');
478 for (i, value) in coords.iter().enumerate() {
479 if i > 0 {
480 s.push_str(", ");
481 }
482 s.push_str(&value.to_string());
483 }
484 s.push(')');
485 s
486 }
487
488 let mut state = ProductState::axis(dimensions[0], periodic[0]);
489 for (&size, &periodic_axis) in dimensions.iter().zip(periodic).skip(1) {
490 let axis = ProductState::axis(size, periodic_axis);
491 state = ProductState::product(&axis, &state)?;
492 }
493
494 let output_sizes = dimensions.iter().rev().copied().collect::<Vec<_>>();
495 let labels = (0..state.node_count)
496 .map(|index| tuple_canonical(&coords_from_index(index, &output_sizes)))
497 .collect::<Vec<_>>();
498 let edge_view = state.edges_view();
499 let mut final_adjacency = vec![Vec::new(); state.node_count];
500 for &(left, right) in &edge_view {
501 final_adjacency[left].push(right);
502 if left != right {
503 final_adjacency[right].push(left);
504 }
505 }
506 let revision = u64::try_from(state.node_count)
507 .unwrap_or(u64::MAX)
508 .saturating_add(u64::try_from(edge_view.len()).unwrap_or(u64::MAX));
509 let mut graph = Self {
510 mode,
511 revision,
512 nodes: FxIndexMap::with_capacity_and_hasher(
513 state.node_count,
514 rustc_hash::FxBuildHasher,
515 ),
516 adj_indices: Vec::with_capacity(state.node_count),
517 all_int_cache: std::sync::Arc::default(),
518 edge_slots_by_neighbor: std::sync::RwLock::default(),
519 edge_index_endpoints: Vec::with_capacity(edge_view.len()),
520 edges: FxIndexMap::with_capacity_and_hasher(edge_view.len(), rustc_hash::FxBuildHasher),
521 runtime_policy: RuntimePolicy::new(mode),
522 };
523
524 for (index, label) in labels.iter().enumerate() {
525 graph.nodes.insert(label.clone(), AttrMap::new());
526 graph.adj_indices.push(final_adjacency[index].clone());
527 }
528
529 for (left, right) in edge_view {
530 graph.edge_index_endpoints.push((left, right));
531 graph
532 .edges
533 .insert(Graph::canon_pair(left, right), AttrMap::new());
534 }
535
536 graph.record_decision(
537 "grid_nd_bulk",
538 0.0,
539 false,
540 vec![EvidenceTerm {
541 signal: "nodes".into(),
542 observed_value: count_evidence(state.node_count),
543 log_likelihood_ratio: 0.0,
544 }],
545 );
546 Ok(graph)
547 }
548
549 #[must_use]
562 pub fn kneser(mode: CompatibilityMode, n: usize, k: usize) -> Self {
563 fn tuple_canonical(c: &[usize]) -> String {
564 let mut s = String::with_capacity(c.len() * 6 + 2);
565 s.push('(');
566 for (i, v) in c.iter().enumerate() {
567 if i > 0 {
568 s.push_str(", ");
569 }
570 s.push_str(&v.to_string());
571 }
572 if c.len() == 1 {
573 s.push(',');
574 }
575 s.push(')');
576 s
577 }
578 fn lex_combinations(m: usize, k: usize) -> Vec<Vec<usize>> {
579 let mut out = Vec::new();
580 if k > m {
581 return out;
582 }
583 let mut cur: Vec<usize> = (0..k).collect();
584 loop {
585 out.push(cur.clone());
586 let mut advanced = false;
587 let mut i = k;
588 while i > 0 {
589 i -= 1;
590 if cur[i] != i + m - k {
591 cur[i] += 1;
592 for j in (i + 1)..k {
593 cur[j] = cur[j - 1] + 1;
594 }
595 advanced = true;
596 break;
597 }
598 }
599 if !advanced {
600 break;
601 }
602 }
603 out
604 }
605 let subsets = lex_combinations(n, k);
606 let canonicals: Vec<String> = subsets.iter().map(|c| tuple_canonical(c)).collect();
607 let total = subsets.len();
608 let mut graph = Self {
609 mode,
610 revision: u64::try_from(total).unwrap_or(u64::MAX),
611 nodes: FxIndexMap::with_capacity_and_hasher(total, rustc_hash::FxBuildHasher),
612 adj_indices: Vec::new(),
613 all_int_cache: std::sync::Arc::default(),
614 edge_slots_by_neighbor: std::sync::RwLock::default(),
615 edge_index_endpoints: Vec::new(),
616 edges: FxIndexMap::default(),
617 runtime_policy: RuntimePolicy::new(mode),
618 };
619 if 2 * k > n {
620 for canonical in &canonicals {
622 graph.nodes.insert(canonical.clone(), AttrMap::new());
623 }
624 graph.adj_indices = vec![Vec::new(); total];
625 return graph;
626 }
627 let mut binom = vec![vec![0usize; k + 1]; n + 1];
629 for i in 0..=n {
630 binom[i][0] = 1;
631 for j in 1..=k.min(i) {
632 binom[i][j] = if i == j {
633 1
634 } else {
635 binom[i - 1][j - 1].saturating_add(binom[i - 1][j])
636 };
637 }
638 }
639 let lex_rank = |c: &[usize]| -> usize {
640 let mut r = 0usize;
641 let mut prev = 0usize;
642 for (i, &ci) in c.iter().enumerate() {
643 for j in prev..ci {
644 r = r.saturating_add(binom[n - 1 - j][k - 1 - i]);
645 }
646 prev = ci + 1;
647 }
648 r
649 };
650 let mut seen = HashSet::<(usize, usize)>::with_capacity(total * 2);
651 let mut node_idx: Vec<usize> = vec![usize::MAX; total];
653 let mut in_s = vec![false; n];
654 let m = n - k;
655 let picks = lex_combinations(m, k);
656 for (s_idx, s) in subsets.iter().enumerate() {
657 for &v in s {
658 in_s[v] = true;
659 }
660 let complement: Vec<usize> = (0..n).filter(|&v| !in_s[v]).collect();
661 for pick in &picks {
662 let t: Vec<usize> = pick.iter().map(|&i| complement[i]).collect();
663 let t_idx = lex_rank(&t);
664 let pair = (s_idx.min(t_idx), s_idx.max(t_idx));
665 if seen.insert(pair) {
666 for idx in [s_idx, t_idx] {
667 if node_idx[idx] == usize::MAX {
668 let entry = graph
669 .nodes
670 .insert_full(canonicals[idx].clone(), AttrMap::new())
671 .0;
672 node_idx[idx] = entry;
673 graph.adj_indices.push(Vec::new());
674 }
675 }
676 graph.adj_indices[node_idx[s_idx]].push(node_idx[t_idx]);
683 if node_idx[s_idx] != node_idx[t_idx] {
684 graph.adj_indices[node_idx[t_idx]].push(node_idx[s_idx]);
685 }
686 graph
687 .edge_index_endpoints
688 .push((node_idx[s_idx], node_idx[t_idx]));
689 graph.edges.insert(
690 Graph::canon_pair(node_idx[s_idx], node_idx[t_idx]),
691 AttrMap::new(),
692 );
693 }
694 }
695 for &v in s {
696 in_s[v] = false;
697 }
698 }
699 graph.revision = u64::try_from(graph.nodes.len() + graph.edges.len()).unwrap_or(u64::MAX);
701 graph
702 }
703
704 #[must_use]
705 pub fn hardened() -> Self {
706 Self::new(CompatibilityMode::Hardened)
707 }
708
709 #[must_use]
710 pub fn mode(&self) -> CompatibilityMode {
711 self.mode
712 }
713
714 #[must_use]
715 pub fn node_count(&self) -> usize {
716 self.nodes.len()
717 }
718
719 #[must_use]
720 pub fn edge_count(&self) -> usize {
721 self.edges.len()
722 }
723
724 #[must_use]
725 pub fn revision(&self) -> u64 {
726 self.revision
727 }
728
729 #[must_use]
730 pub fn has_node(&self, node: &str) -> bool {
731 self.nodes.contains_key(node)
732 }
733
734 #[inline]
737 fn edge_pair_key(&self, left: &str, right: &str) -> Option<(usize, usize)> {
738 let l = self.nodes.get_index_of(left)?;
739 let r = self.nodes.get_index_of(right)?;
740 Some(if l <= r { (l, r) } else { (r, l) })
741 }
742
743 #[inline]
744 fn canon_pair(l: usize, r: usize) -> (usize, usize) {
745 if l <= r { (l, r) } else { (r, l) }
746 }
747
748 #[must_use]
773 pub fn induced_subgraph_ordered(&self, order: &[usize], runtime_policy: RuntimePolicy) -> Self {
774 let mut position = vec![usize::MAX; self.nodes.len()];
775 for (slot, &parent_idx) in order.iter().enumerate() {
776 position[parent_idx] = slot;
777 }
778
779 let mut nodes = FxIndexMap::default();
780 nodes.reserve(order.len());
781 for &parent_idx in order {
782 let (name, attrs) = self
783 .nodes
784 .get_index(parent_idx)
785 .expect("induced subgraph order must hold valid parent node indices");
786 nodes.insert(name.clone(), attrs.clone());
787 }
788
789 let mut adj_indices = vec![Vec::new(); order.len()];
790 let mut edges = FxIndexMap::default();
791 let mut edge_index_endpoints = Vec::new();
792 for (slot, &parent_idx) in order.iter().enumerate() {
793 for &parent_neighbor in &self.adj_indices[parent_idx] {
794 let other = position[parent_neighbor];
795 if other == usize::MAX || other < slot {
796 continue;
797 }
798 let attrs = self
799 .edges
800 .get(&Self::canon_pair(parent_idx, parent_neighbor))
801 .cloned()
802 .unwrap_or_default();
803 edges.insert(Self::canon_pair(slot, other), attrs);
804 let left_name = nodes.get_index(slot).expect("slot inserted above").0;
805 let right_name = nodes.get_index(other).expect("slot inserted above").0;
806 if left_name <= right_name {
807 edge_index_endpoints.push((slot, other));
808 } else {
809 edge_index_endpoints.push((other, slot));
810 }
811 adj_indices[slot].push(other);
812 if slot != other {
813 adj_indices[other].push(slot);
814 }
815 }
816 }
817
818 Self {
819 mode: runtime_policy.mode(),
820 revision: 0,
821 nodes,
822 adj_indices,
823 all_int_cache: std::sync::Arc::default(),
824 edge_slots_by_neighbor: std::sync::RwLock::default(),
825 edge_index_endpoints,
826 edges,
827 runtime_policy,
828 }
829 }
830
831 #[must_use]
832 pub fn has_edge(&self, left: &str, right: &str) -> bool {
833 self.edge_pair_key(left, right)
834 .is_some_and(|k| self.edges.contains_key(&k))
835 }
836
837 #[must_use]
844 pub fn node_index_matches_int(&self, idx: usize) -> bool {
845 self.nodes
846 .get_index(idx)
847 .is_some_and(|(k, _)| k.parse::<usize>() == Ok(idx))
848 }
849
850 #[must_use]
854 pub fn has_edge_by_indices(&self, l: usize, r: usize) -> bool {
855 self.edges.contains_key(&Self::canon_pair(l, r))
856 }
857
858 #[must_use]
859 pub fn nodes_ordered(&self) -> Vec<&str> {
860 self.nodes.keys().map(String::as_str).collect()
861 }
862
863 #[must_use]
864 pub fn get_node_index(&self, node: &str) -> Option<usize> {
865 self.nodes.get_index_of(node)
866 }
867
868 #[must_use]
869 pub fn get_node_name(&self, index: usize) -> Option<&str> {
870 self.nodes.get_index(index).map(|(k, _)| k.as_str())
871 }
872
873 #[must_use]
874 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
875 let idx = self.nodes.get_index_of(node)?;
882 Some(
883 self.adj_indices[idx]
884 .iter()
885 .map(|&i| {
886 self.nodes
887 .get_index(i)
888 .expect("adj_indices entries are valid node indices")
889 .0
890 .as_str()
891 })
892 .collect::<Vec<&str>>(),
893 )
894 }
895
896 #[must_use]
897 pub fn neighbors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
898 let idx = self.nodes.get_index_of(node)?;
900 Some(self.adj_indices[idx].iter().map(move |&i| {
901 self.nodes
902 .get_index(i)
903 .expect("adj_indices entries are valid node indices")
904 .0
905 .as_str()
906 }))
907 }
908
909 #[must_use]
912 #[inline]
913 pub fn neighbors_indices(&self, node_idx: usize) -> Option<&[usize]> {
914 self.adj_indices.get(node_idx).map(Vec::as_slice)
915 }
916
917 #[must_use]
922 pub fn clone_with_fresh_policy(&self) -> Self {
923 Self {
924 mode: self.mode,
925 revision: self.revision,
926 nodes: self.nodes.clone(),
927 adj_indices: self.adj_indices.clone(),
928 all_int_cache: std::sync::Arc::default(),
929 edge_slots_by_neighbor: std::sync::RwLock::default(),
930 edge_index_endpoints: self.edge_index_endpoints.clone(),
931 edges: self.edges.clone(),
932 runtime_policy: RuntimePolicy::new(self.mode),
933 }
934 }
935
936 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)]) {
942 let mut changed = false;
944 for (node, order) in orders {
945 let Some(idx) = self.nodes.get_index_of(node.as_str()) else {
946 continue;
947 };
948 let row = &self.adj_indices[idx];
949 let row_set: std::collections::HashSet<usize> = row.iter().copied().collect();
950 let mut new_row: Vec<usize> = Vec::with_capacity(row.len());
951 let mut placed: std::collections::HashSet<usize> = std::collections::HashSet::new();
952 for v in order {
953 if let Some(v_idx) = self.nodes.get_index_of(v.as_str())
954 && row_set.contains(&v_idx)
955 && placed.insert(v_idx)
956 {
957 new_row.push(v_idx);
958 }
959 }
960 for &v_idx in row {
961 if placed.insert(v_idx) {
962 new_row.push(v_idx);
963 }
964 }
965 if self.adj_indices[idx] != new_row {
966 self.adj_indices[idx] = new_row;
967 changed = true;
968 }
969 }
970 if changed {
971 self.revision = self.revision.saturating_add(1);
972 }
973 }
974
975 pub fn reorder_rows_for_nx_copy_walk(&mut self) {
986 let n = self.adj_indices.len();
996 let mut early: Vec<Vec<usize>> = vec![Vec::new(); n];
997 for (v, row) in self.adj_indices.iter().enumerate() {
998 for &pu in row {
999 if pu > v {
1000 early[pu].push(v);
1001 }
1002 }
1003 }
1004 let mut new_rows: Vec<Vec<usize>> = Vec::with_capacity(n);
1005 for (pu, row) in self.adj_indices.iter().enumerate() {
1006 let mut new_row = std::mem::take(&mut early[pu]);
1007 new_row.reserve(row.len());
1008 for &v in row {
1009 if v >= pu {
1010 new_row.push(v);
1011 }
1012 }
1013 new_rows.push(new_row);
1014 }
1015 if self.adj_indices != new_rows {
1016 self.adj_indices = new_rows;
1017 self.revision = self.revision.saturating_add(1);
1018 }
1019 }
1020
1021 pub fn edges_storage_order_index_iter(
1022 &self,
1023 ) -> impl Iterator<Item = (usize, usize, &AttrMap)> + '_ {
1024 self.edge_index_endpoints
1025 .iter()
1026 .copied()
1027 .zip(self.edges.values())
1028 .map(|((left, right), attrs)| (left, right, attrs))
1029 }
1030
1031 #[must_use]
1032 pub fn neighbor_count(&self, node: &str) -> usize {
1033 self.nodes
1035 .get_index_of(node)
1036 .map_or(0, |idx| self.adj_indices[idx].len())
1037 }
1038
1039 #[must_use]
1042 pub fn degree(&self, node: &str) -> usize {
1043 let count = self.neighbor_count(node);
1044 if self.has_edge(node, node) {
1046 count + 1
1047 } else {
1048 count
1049 }
1050 }
1051
1052 #[must_use]
1057 pub fn degree_by_index(&self, idx: usize) -> usize {
1058 let mut count = self.adj_indices[idx].len();
1065 if self.has_edge_by_indices(idx, idx) {
1066 count += 1; }
1068 count
1069 }
1070
1071 #[must_use]
1072 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
1073 self.nodes.get(node)
1074 }
1075
1076 #[must_use]
1077 pub fn edge_attrs(&self, left: &str, right: &str) -> Option<&AttrMap> {
1078 self.edges.get(&self.edge_pair_key(left, right)?)
1079 }
1080
1081 #[must_use]
1085 #[inline]
1086 pub fn edge_attrs_by_indices(&self, left_idx: usize, right_idx: usize) -> Option<&AttrMap> {
1087 self.edges.get(&Self::canon_pair(left_idx, right_idx))
1088 }
1089
1090 #[must_use]
1093 #[inline]
1094 pub fn edge_attrs_by_slot(&self, edge_slot: usize) -> Option<&AttrMap> {
1095 self.edges.get_index(edge_slot).map(|(_, attrs)| attrs)
1096 }
1097
1098 pub fn with_edge_slots_by_neighbor<T>(&self, f: impl FnOnce(&[Vec<usize>]) -> T) -> T {
1102 if let Ok(guard) = self.edge_slots_by_neighbor.read()
1103 && let Some((revision, rows)) = guard.as_ref()
1104 && *revision == self.revision
1105 {
1106 return f(rows);
1107 }
1108
1109 let mut guard = self
1110 .edge_slots_by_neighbor
1111 .write()
1112 .expect("edge slot cache lock poisoned");
1113 if !matches!(guard.as_ref(), Some((revision, _)) if *revision == self.revision) {
1114 let mut rows = Vec::with_capacity(self.adj_indices.len());
1115 for (node, neighbors) in self.adj_indices.iter().enumerate() {
1116 let mut slots = Vec::with_capacity(neighbors.len());
1117 for &neighbor in neighbors {
1118 let slot = self
1119 .edges
1120 .get_index_of(&Self::canon_pair(node, neighbor))
1121 .expect("adjacency entries have matching edge storage");
1122 slots.push(slot);
1123 }
1124 rows.push(slots);
1125 }
1126 *guard = Some((self.revision, rows));
1127 }
1128 let (_, rows) = guard
1129 .as_ref()
1130 .expect("edge slot cache is initialized before use");
1131 f(rows)
1132 }
1133
1134 #[must_use]
1140 pub fn any_edge_has_attr(&self, key: &str) -> bool {
1141 self.edges.values().any(|attrs| attrs.contains_key(key))
1142 }
1143
1144 #[must_use]
1150 pub fn all_edge_attr_values_scalar(&self) -> bool {
1151 self.edges.values().all(|attrs| {
1152 attrs.values().all(|v| {
1153 matches!(
1154 v,
1155 fnx_runtime::CgseValue::Int(_)
1156 | fnx_runtime::CgseValue::Float(_)
1157 | fnx_runtime::CgseValue::Bool(_)
1158 )
1159 })
1160 })
1161 }
1162
1163 #[must_use]
1166 pub fn any_node_has_attrs(&self) -> bool {
1167 self.nodes.values().any(|attrs| !attrs.is_empty())
1168 }
1169
1170 #[must_use]
1173 pub fn any_edge_has_attrs(&self) -> bool {
1174 self.edges.values().any(|attrs| !attrs.is_empty())
1175 }
1176
1177 #[must_use]
1178 pub fn nodes_are_contiguous_int_prefix(&self) -> bool {
1179 self.nodes
1180 .keys()
1181 .enumerate()
1182 .all(|(index, key)| Self::decimal_key_matches_index(key, index))
1183 }
1184
1185 fn decimal_key_matches_index(key: &str, index: usize) -> bool {
1186 let bytes = key.as_bytes();
1187 if bytes.is_empty() || (bytes.len() > 1 && bytes[0] == b'0') {
1188 return false;
1189 }
1190
1191 let mut parsed = 0usize;
1192 for &byte in bytes {
1193 if !byte.is_ascii_digit() {
1194 return false;
1195 }
1196 let digit = usize::from(byte - b'0');
1197 let Some(next) = parsed
1198 .checked_mul(10)
1199 .and_then(|value| value.checked_add(digit))
1200 else {
1201 return false;
1202 };
1203 parsed = next;
1204 }
1205 parsed == index
1206 }
1207
1208 #[must_use]
1209 pub fn evidence_ledger(&self) -> &EvidenceLedger {
1210 self.runtime_policy.decision_log()
1211 }
1212
1213 pub fn drain_decision_records(&mut self) -> Vec<DecisionRecord> {
1214 self.runtime_policy.drain_decision_records()
1215 }
1216
1217 #[must_use]
1218 pub fn runtime_policy(&self) -> &RuntimePolicy {
1219 &self.runtime_policy
1220 }
1221
1222 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
1223 self.mode = runtime_policy.mode();
1224 self.runtime_policy = runtime_policy;
1225 }
1226
1227 #[must_use]
1229 pub fn is_directed(&self) -> bool {
1230 false
1231 }
1232
1233 #[must_use]
1235 pub fn is_multigraph(&self) -> bool {
1236 false
1237 }
1238
1239 pub fn add_node(&mut self, node: impl AsRef<str> + Into<String>) -> bool {
1240 self.add_node_with_attrs(node, AttrMap::new())
1241 }
1242
1243 fn add_node_with_attrs_unrecorded(
1266 &mut self,
1267 node: impl AsRef<str> + Into<String>,
1268 attrs: AttrMap,
1269 ) -> (bool, bool, usize, Option<usize>) {
1270 let node_hash = self.nodes.hasher().hash_one(node.as_ref());
1271 let (existed, changed, attrs_count, inserted_index) = match self
1272 .nodes
1273 .raw_entry_mut_v1()
1274 .from_key_hashed_nocheck(node_hash, node.as_ref())
1275 {
1276 RawEntryMut::Occupied(mut entry) => {
1277 let bucket = entry.get_mut();
1278 let changed = !attrs.is_empty()
1279 && attrs
1280 .iter()
1281 .any(|(key, value)| bucket.get(key) != Some(value));
1282 bucket.extend(attrs);
1283 (true, changed, bucket.len(), None)
1284 }
1285 RawEntryMut::Vacant(entry) => {
1286 let index = entry.index();
1287 let (_, bucket) =
1288 entry.insert_hashed_nocheck(node_hash, node.into(), AttrMap::new());
1289 bucket.extend(attrs);
1290 (false, true, bucket.len(), Some(index))
1291 }
1292 };
1293 if !existed {
1295 self.adj_indices.push(Vec::new());
1296 }
1297 if changed {
1298 self.revision = self.revision.saturating_add(1);
1299 }
1300 (changed, existed, attrs_count, inserted_index)
1301 }
1302
1303 fn resolve_empty_node_for_edge_unrecorded(&mut self, node: &str) -> (usize, bool) {
1319 let node_hash = self.nodes.hasher().hash_one(node);
1320 match self
1321 .nodes
1322 .raw_entry_mut_v1()
1323 .from_key_hashed_nocheck(node_hash, node)
1324 {
1325 RawEntryMut::Occupied(entry) => (entry.index(), false),
1326 RawEntryMut::Vacant(entry) => {
1327 let index = entry.index();
1328 entry.insert_hashed_nocheck(node_hash, node.to_owned(), AttrMap::new());
1329 self.adj_indices.push(Vec::new());
1330 self.revision = self.revision.saturating_add(1);
1331 (index, true)
1332 }
1333 }
1334 }
1335
1336 pub fn add_node_with_attrs(
1337 &mut self,
1338 node: impl AsRef<str> + Into<String>,
1339 attrs: AttrMap,
1340 ) -> bool {
1341 let (changed, existed, attrs_count, _) = self.add_node_with_attrs_unrecorded(node, attrs);
1342 self.record_decision(
1343 "add_node",
1344 0.0,
1345 false,
1346 vec![
1347 EvidenceTerm {
1348 signal: "node_preexisting".into(),
1349 observed_value: bool_evidence(existed),
1350 log_likelihood_ratio: -3.0,
1351 },
1352 EvidenceTerm {
1353 signal: "attrs_count".into(),
1354 observed_value: count_evidence(attrs_count),
1355 log_likelihood_ratio: -1.0,
1356 },
1357 ],
1358 );
1359 changed
1360 }
1361
1362 #[must_use]
1365 pub fn extend_nodes_unrecorded<I, N>(&mut self, nodes: I) -> usize
1366 where
1367 I: IntoIterator<Item = N>,
1368 N: Into<String>,
1369 {
1370 let iterator = nodes.into_iter();
1371 let (lower_bound, _) = iterator.size_hint();
1372 self.nodes.reserve(lower_bound);
1373 self.adj_indices.reserve(lower_bound);
1374
1375 let mut inserted = 0usize;
1376 for node in iterator {
1377 let node = node.into();
1378 if self.nodes.contains_key(&node) {
1379 continue;
1380 }
1381 self.nodes.insert(node.clone(), AttrMap::new());
1382 self.adj_indices.push(Vec::new());
1383 inserted += 1;
1384 }
1385 if inserted > 0 {
1386 self.revision = self
1387 .revision
1388 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1389 self.record_decision(
1390 "extend_nodes_unrecorded",
1391 0.0,
1392 false,
1393 vec![EvidenceTerm {
1394 signal: "batch_node_count".into(),
1395 observed_value: count_evidence(inserted),
1396 log_likelihood_ratio: -1.0,
1397 }],
1398 );
1399 }
1400 inserted
1401 }
1402
1403 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
1407 where
1408 I: IntoIterator<Item = (String, AttrMap)>,
1409 {
1410 let iterator = nodes.into_iter();
1411 let (lower_bound, _) = iterator.size_hint();
1412 self.nodes.reserve(lower_bound);
1413 self.adj_indices.reserve(lower_bound);
1414
1415 let mut inserted = 0usize;
1416 for (node, attrs) in iterator {
1417 if let Some(existing) = self.nodes.get_mut(&node) {
1418 existing.extend(attrs);
1419 continue;
1420 }
1421 self.nodes.insert(node.clone(), attrs);
1422 self.adj_indices.push(Vec::new());
1423 inserted += 1;
1424 }
1425 if inserted > 0 {
1426 self.revision = self
1427 .revision
1428 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1429 self.record_decision(
1430 "extend_nodes_with_attrs_unrecorded",
1431 0.0,
1432 false,
1433 vec![EvidenceTerm {
1434 signal: "batch_node_count".into(),
1435 observed_value: count_evidence(inserted),
1436 log_likelihood_ratio: -1.0,
1437 }],
1438 );
1439 }
1440 inserted
1441 }
1442
1443 pub fn apply_node_defaults(&mut self, defaults: &AttrMap) -> bool {
1444 if defaults.is_empty() {
1445 return false;
1446 }
1447 let mut inserted = 0usize;
1448 for attrs in self.nodes.values_mut() {
1449 for (key, value) in defaults {
1450 if !attrs.contains_key(key) {
1451 attrs.insert(key.clone(), value.clone());
1452 inserted += 1;
1453 }
1454 }
1455 }
1456 if inserted > 0 {
1457 self.revision = self.revision.saturating_add(1);
1458 self.record_decision(
1459 "apply_node_defaults",
1460 0.0,
1461 false,
1462 vec![EvidenceTerm {
1463 signal: "defaults_applied".into(),
1464 observed_value: count_evidence(inserted),
1465 log_likelihood_ratio: 0.0,
1466 }],
1467 );
1468 return true;
1469 }
1470 false
1471 }
1472
1473 pub fn add_edge(
1474 &mut self,
1475 left: impl AsRef<str>,
1476 right: impl AsRef<str>,
1477 ) -> Result<(), GraphError> {
1478 self.add_edge_with_attrs(left, right, AttrMap::new())
1479 }
1480
1481 #[must_use]
1496 pub fn extend_edges_unrecorded<I, L, R>(&mut self, edges: I) -> usize
1497 where
1498 I: IntoIterator<Item = (L, R)>,
1499 L: Into<String>,
1500 R: Into<String>,
1501 {
1502 let iterator = edges.into_iter();
1503 let (lower_bound, _) = iterator.size_hint();
1504 self.nodes.reserve(lower_bound);
1505 self.adj_indices.reserve(lower_bound);
1506 self.edges.reserve(lower_bound);
1507 self.edge_index_endpoints.reserve(lower_bound);
1508
1509 let mut inserted = 0usize;
1510 let mut nodes_added = false;
1511 for (left, right) in iterator {
1512 let left = left.into();
1513 let right = right.into();
1514 let left_idx = match self.nodes.get_index_of(&left) {
1515 Some(index) => index,
1516 None => {
1517 let index = self.nodes.len();
1518 self.nodes.insert(left.clone(), AttrMap::new());
1519 self.adj_indices.push(Vec::new());
1520 nodes_added = true;
1521 index
1522 }
1523 };
1524 let right_idx = if left == right {
1525 left_idx
1526 } else {
1527 match self.nodes.get_index_of(&right) {
1528 Some(index) => index,
1529 None => {
1530 let index = self.nodes.len();
1531 self.nodes.insert(right.clone(), AttrMap::new());
1532 self.adj_indices.push(Vec::new());
1533 nodes_added = true;
1534 index
1535 }
1536 }
1537 };
1538 let edge_key = Self::canon_pair(left_idx, right_idx);
1539 if self.edges.contains_key(&edge_key) {
1540 continue;
1541 }
1542 if left <= right {
1543 self.edge_index_endpoints.push((left_idx, right_idx));
1544 } else {
1545 self.edge_index_endpoints.push((right_idx, left_idx));
1546 }
1547 self.edges.insert(edge_key, AttrMap::new());
1548
1549 self.adj_indices[left_idx].push(right_idx);
1550 if left_idx != right_idx {
1551 self.adj_indices[right_idx].push(left_idx);
1552 }
1553 inserted += 1;
1554 }
1555 self.record_bulk_edge_summary(inserted, nodes_added);
1556 inserted
1557 }
1558
1559 #[must_use]
1565 pub fn extend_existing_index_edges_unrecorded<I>(&mut self, edges: I) -> usize
1566 where
1567 I: IntoIterator<Item = (usize, usize)>,
1568 {
1569 let iterator = edges.into_iter();
1570 let (lower_bound, _) = iterator.size_hint();
1571 self.edges.reserve(lower_bound);
1572 self.edge_index_endpoints.reserve(lower_bound);
1573
1574 let mut inserted = 0usize;
1575 for (left_idx, right_idx) in iterator {
1576 debug_assert!(left_idx < self.nodes.len());
1577 debug_assert!(right_idx < self.nodes.len());
1578
1579 let edge_key = Self::canon_pair(left_idx, right_idx);
1580 if self.edges.contains_key(&edge_key) {
1581 continue;
1582 }
1583
1584 let left_name = self
1585 .nodes
1586 .get_index(left_idx)
1587 .expect("validated left node index")
1588 .0
1589 .as_str();
1590 let right_name = self
1591 .nodes
1592 .get_index(right_idx)
1593 .expect("validated right node index")
1594 .0
1595 .as_str();
1596 if left_name <= right_name {
1597 self.edge_index_endpoints.push((left_idx, right_idx));
1598 } else {
1599 self.edge_index_endpoints.push((right_idx, left_idx));
1600 }
1601 self.edges.insert(edge_key, AttrMap::new());
1602
1603 self.adj_indices[left_idx].push(right_idx);
1604 if left_idx != right_idx {
1605 self.adj_indices[right_idx].push(left_idx);
1606 }
1607 inserted += 1;
1608 }
1609 self.record_bulk_edge_summary(inserted, false);
1610 inserted
1611 }
1612
1613 pub fn extend_existing_index_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
1621 where
1622 I: IntoIterator<Item = (usize, usize, AttrMap)>,
1623 {
1624 let iterator = edges.into_iter();
1625 let (lower_bound, _) = iterator.size_hint();
1626 self.edges.reserve(lower_bound);
1627 self.edge_index_endpoints.reserve(lower_bound);
1628
1629 let mut inserted = 0usize;
1630 for (left_idx, right_idx, attrs) in iterator {
1631 debug_assert!(left_idx < self.nodes.len());
1632 debug_assert!(right_idx < self.nodes.len());
1633
1634 let edge_key = Self::canon_pair(left_idx, right_idx);
1635 if let Some(existing) = self.edges.get_mut(&edge_key) {
1636 for (key, value) in attrs {
1638 existing.insert(key, value);
1639 }
1640 continue;
1641 }
1642
1643 let left_name = self
1644 .nodes
1645 .get_index(left_idx)
1646 .expect("validated left node index")
1647 .0
1648 .as_str();
1649 let right_name = self
1650 .nodes
1651 .get_index(right_idx)
1652 .expect("validated right node index")
1653 .0
1654 .as_str();
1655 if left_name <= right_name {
1656 self.edge_index_endpoints.push((left_idx, right_idx));
1657 } else {
1658 self.edge_index_endpoints.push((right_idx, left_idx));
1659 }
1660 self.edges.insert(edge_key, attrs);
1661
1662 self.adj_indices[left_idx].push(right_idx);
1663 if left_idx != right_idx {
1664 self.adj_indices[right_idx].push(left_idx);
1665 }
1666 inserted += 1;
1667 }
1668 self.record_bulk_edge_summary(inserted, false);
1669 inserted
1670 }
1671
1672 pub fn extend_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
1687 where
1688 I: IntoIterator<Item = (String, String, AttrMap)>,
1689 {
1690 let iterator = edges.into_iter();
1691 let (lower_bound, _) = iterator.size_hint();
1692 self.nodes.reserve(lower_bound);
1693 self.adj_indices.reserve(lower_bound);
1694 self.edges.reserve(lower_bound);
1695 self.edge_index_endpoints.reserve(lower_bound);
1696
1697 let mut inserted = 0usize;
1698 let mut nodes_added = false;
1699 let mut merged_changed = false;
1700 for (left, right, attrs) in iterator {
1701 let left_idx = match self.nodes.get_index_of(&left) {
1702 Some(index) => index,
1703 None => {
1704 let index = self.nodes.len();
1705 self.nodes.insert(left.clone(), AttrMap::new());
1706 self.adj_indices.push(Vec::new());
1707 nodes_added = true;
1708 index
1709 }
1710 };
1711 let right_idx = if left == right {
1712 left_idx
1713 } else {
1714 match self.nodes.get_index_of(&right) {
1715 Some(index) => index,
1716 None => {
1717 let index = self.nodes.len();
1718 self.nodes.insert(right.clone(), AttrMap::new());
1719 self.adj_indices.push(Vec::new());
1720 nodes_added = true;
1721 index
1722 }
1723 }
1724 };
1725 let edge_key = Self::canon_pair(left_idx, right_idx);
1726 if let Some(existing) = self.edges.get_mut(&edge_key) {
1727 if attrs.is_empty() {
1733 continue;
1734 }
1735 if attrs
1736 .iter()
1737 .any(|(key, value)| existing.get(key) != Some(value))
1738 {
1739 merged_changed = true;
1740 }
1741 existing.extend(attrs);
1742 continue;
1743 }
1744 if left <= right {
1745 self.edge_index_endpoints.push((left_idx, right_idx));
1746 } else {
1747 self.edge_index_endpoints.push((right_idx, left_idx));
1748 }
1749 self.edges.insert(edge_key, attrs);
1750
1751 self.adj_indices[left_idx].push(right_idx);
1752 if left_idx != right_idx {
1753 self.adj_indices[right_idx].push(left_idx);
1754 }
1755 inserted += 1;
1756 }
1757 if merged_changed {
1758 self.revision = self.revision.saturating_add(1);
1762 }
1763 self.record_bulk_edge_summary(inserted, nodes_added || merged_changed);
1764 inserted
1765 }
1766
1767 #[must_use]
1776 pub fn extend_fresh_index_edges_with_attrs_unrecorded<I, N>(
1777 &mut self,
1778 nodes: N,
1779 edges: I,
1780 ) -> usize
1781 where
1782 I: IntoIterator<Item = (usize, usize, AttrMap)>,
1783 N: IntoIterator<Item = String>,
1784 {
1785 if !self.nodes.is_empty()
1786 || !self.adj_indices.is_empty()
1787 || !self.edges.is_empty()
1788 || !self.edge_index_endpoints.is_empty()
1789 {
1790 return 0;
1791 }
1792
1793 let node_labels: Vec<String> = nodes.into_iter().collect();
1794 self.nodes.reserve(node_labels.len());
1795 self.adj_indices = vec![Vec::new(); node_labels.len()];
1796 for node in &node_labels {
1797 self.nodes.insert(node.clone(), AttrMap::new());
1798 }
1799
1800 let iterator = edges.into_iter();
1801 let (lower_bound, _) = iterator.size_hint();
1802 self.edges.reserve(lower_bound);
1803 self.edge_index_endpoints.reserve(lower_bound);
1804
1805 let node_count = node_labels.len();
1806 let mut inserted = 0usize;
1807 let mut merged_changed = false;
1808 for (left_idx, right_idx, attrs) in iterator {
1809 if left_idx >= node_count || right_idx >= node_count {
1810 continue;
1811 }
1812
1813 let edge_key = Self::canon_pair(left_idx, right_idx);
1814 if let Some(existing) = self.edges.get_mut(&edge_key) {
1815 if attrs.is_empty() {
1819 continue;
1820 }
1821 if attrs
1822 .iter()
1823 .any(|(key, value)| existing.get(key) != Some(value))
1824 {
1825 merged_changed = true;
1826 }
1827 existing.extend(attrs);
1828 continue;
1829 }
1830
1831 if node_labels[left_idx] <= node_labels[right_idx] {
1832 self.edge_index_endpoints.push((left_idx, right_idx));
1833 } else {
1834 self.edge_index_endpoints.push((right_idx, left_idx));
1835 }
1836 self.edges.insert(edge_key, attrs);
1837 self.adj_indices[left_idx].push(right_idx);
1838 if left_idx != right_idx {
1839 self.adj_indices[right_idx].push(left_idx);
1840 }
1841 inserted += 1;
1842 }
1843
1844 self.record_bulk_edge_summary(inserted, node_count > 0 || merged_changed);
1845 inserted
1846 }
1847
1848 fn record_bulk_edge_summary(&mut self, inserted: usize, nodes_added: bool) {
1849 if inserted == 0 && !nodes_added {
1850 return;
1851 }
1852 self.revision = self
1853 .revision
1854 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
1855 self.record_decision(
1859 "extend_edges_unrecorded",
1860 0.0,
1861 false,
1862 vec![EvidenceTerm {
1863 signal: "batch_edge_count".into(),
1864 observed_value: count_evidence(inserted),
1865 log_likelihood_ratio: -1.0,
1866 }],
1867 );
1868 }
1869
1870 pub fn add_edge_with_attrs(
1885 &mut self,
1886 left: impl AsRef<str>,
1887 right: impl AsRef<str>,
1888 attrs: AttrMap,
1889 ) -> Result<(), GraphError> {
1890 let left = left.as_ref();
1891 let right = right.as_ref();
1892
1893 let unknown_feature = attrs
1894 .keys()
1895 .any(|key| key.starts_with("__fnx_incompatible"));
1896 let incompatibility_probability = if unknown_feature {
1897 1.0
1898 } else if left == right {
1899 0.22
1900 } else {
1901 0.08
1902 };
1903
1904 let action = self
1912 .runtime_policy
1913 .action_for(incompatibility_probability, unknown_feature);
1914
1915 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
1916 self.record_decision(
1917 "add_edge",
1918 incompatibility_probability,
1919 unknown_feature,
1920 vec![EvidenceTerm {
1921 signal: "unknown_incompatible_feature".into(),
1922 observed_value: bool_evidence(unknown_feature),
1923 log_likelihood_ratio: 12.0,
1924 }],
1925 );
1926 return Err(GraphError::FailClosed {
1927 operation: "add_edge",
1928 reason: "incompatible edge metadata".to_owned(),
1929 });
1930 }
1931
1932 let (left_key_idx, left_autocreated) = self.resolve_empty_node_for_edge_unrecorded(left);
1954 let (right_key_idx, right_autocreated) = if left == right {
1955 (left_key_idx, left_autocreated)
1956 } else {
1957 self.resolve_empty_node_for_edge_unrecorded(right)
1958 };
1959 let edge_key = Self::canon_pair(left_key_idx, right_key_idx);
1960 let self_loop = left == right;
1961 let (new_edge, changed, edge_attr_count) = match self.edges.entry(edge_key) {
1966 indexmap::map::Entry::Vacant(entry) => {
1967 let edge_attr_count = attrs.len();
1968 entry.insert(attrs);
1969 (true, true, edge_attr_count)
1970 }
1971 indexmap::map::Entry::Occupied(mut entry) => {
1972 let edge_attrs = entry.get_mut();
1973 let changed = !attrs.is_empty()
1974 && attrs
1975 .iter()
1976 .any(|(key, value)| edge_attrs.get(key) != Some(value));
1977 edge_attrs.extend(attrs);
1978 (false, changed, edge_attrs.len())
1979 }
1980 };
1981
1982 if changed {
1984 if new_edge {
1993 if left <= right {
1994 self.edge_index_endpoints
1995 .push((left_key_idx, right_key_idx));
1996 } else {
1997 self.edge_index_endpoints
1998 .push((right_key_idx, left_key_idx));
1999 }
2000 self.adj_indices[left_key_idx].push(right_key_idx);
2009 if left_key_idx != right_key_idx {
2010 self.adj_indices[right_key_idx].push(left_key_idx);
2011 }
2012 }
2013 self.revision = self.revision.saturating_add(1);
2014 }
2015
2016 self.record_decision(
2017 "add_edge",
2018 incompatibility_probability,
2019 unknown_feature,
2020 vec![
2021 EvidenceTerm {
2022 signal: "self_loop".into(),
2023 observed_value: bool_evidence(self_loop),
2024 log_likelihood_ratio: -0.5,
2025 },
2026 EvidenceTerm {
2027 signal: "edge_attr_count".into(),
2028 observed_value: count_evidence(edge_attr_count),
2029 log_likelihood_ratio: -2.0,
2030 },
2031 EvidenceTerm {
2032 signal: "left_autocreated".into(),
2033 observed_value: bool_evidence(left_autocreated),
2034 log_likelihood_ratio: -1.25,
2035 },
2036 EvidenceTerm {
2037 signal: "right_autocreated".into(),
2038 observed_value: bool_evidence(right_autocreated),
2039 log_likelihood_ratio: -1.25,
2040 },
2041 ],
2042 );
2043
2044 Ok(())
2045 }
2046
2047 pub fn apply_edge_defaults(&mut self, defaults: &AttrMap) -> bool {
2048 if defaults.is_empty() {
2049 return false;
2050 }
2051 let mut inserted = 0usize;
2052 for attrs in self.edges.values_mut() {
2053 for (key, value) in defaults {
2054 if !attrs.contains_key(key) {
2055 attrs.insert(key.clone(), value.clone());
2056 inserted += 1;
2057 }
2058 }
2059 }
2060 if inserted > 0 {
2061 self.revision = self.revision.saturating_add(1);
2062 self.record_decision(
2063 "apply_edge_defaults",
2064 0.0,
2065 false,
2066 vec![EvidenceTerm {
2067 signal: "defaults_applied".into(),
2068 observed_value: count_evidence(inserted),
2069 log_likelihood_ratio: 0.0,
2070 }],
2071 );
2072 return true;
2073 }
2074 false
2075 }
2076
2077 pub fn replace_edge_attrs(&mut self, left: &str, right: &str, attrs: AttrMap) -> bool {
2082 let Some(edge_key) = self.edge_pair_key(left, right) else {
2083 return false;
2084 };
2085 if let Some(slot) = self.edges.get_mut(&edge_key) {
2086 if *slot != attrs {
2087 *slot = attrs;
2088 self.revision = self.revision.saturating_add(1);
2089 }
2090 true
2091 } else {
2092 false
2093 }
2094 }
2095
2096 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
2098 if let Some(slot) = self.nodes.get_mut(node) {
2099 if *slot != attrs {
2100 *slot = attrs;
2101 self.revision = self.revision.saturating_add(1);
2102 }
2103 true
2104 } else {
2105 false
2106 }
2107 }
2108
2109 pub fn remove_edge(&mut self, left: &str, right: &str) -> bool {
2110 let Some(pair) = self.edge_pair_key(left, right) else {
2119 return false;
2120 };
2121 let removed = self.edges.swap_remove_full(&pair);
2132 if let Some((edge_pos, _, _)) = removed {
2133 if let (Some(left_idx), Some(right_idx)) = (
2136 self.nodes.get_index_of(left),
2137 self.nodes.get_index_of(right),
2138 ) {
2139 self.adj_indices[left_idx].retain(|&i| i != right_idx);
2140 if left_idx != right_idx {
2141 self.adj_indices[right_idx].retain(|&i| i != left_idx);
2142 }
2143 }
2144 self.edge_index_endpoints.swap_remove(edge_pos);
2145 self.revision = self.revision.saturating_add(1);
2146 true
2147 } else {
2148 false
2149 }
2150 }
2151
2152 pub fn remove_node(&mut self, node: &str) -> bool {
2153 let Some(idx) = self.nodes.get_index_of(node) else {
2154 return false;
2155 };
2156
2157 if idx + 1 == self.nodes.len() && self.adj_indices[idx].is_empty() {
2203 self.adj_indices.remove(idx);
2204 self.nodes.shift_remove(node);
2205 self.revision = self.revision.saturating_add(1);
2206 return true;
2207 }
2208
2209 self.adj_indices.remove(idx);
2212 self.nodes.shift_remove(node);
2214 for nbrs in &mut self.adj_indices {
2217 let mut w = 0usize;
2218 for read in 0..nbrs.len() {
2219 let e = nbrs[read];
2220 if e == idx {
2221 continue;
2222 }
2223 nbrs[w] = if e > idx { e - 1 } else { e };
2224 w += 1;
2225 }
2226 nbrs.truncate(w);
2227 }
2228 let old_edges = std::mem::take(&mut self.edges);
2233 let old_endpoints = std::mem::take(&mut self.edge_index_endpoints);
2234 let mut new_edges =
2235 FxIndexMap::with_capacity_and_hasher(old_edges.len(), rustc_hash::FxBuildHasher);
2236 let mut new_endpoints = Vec::with_capacity(old_endpoints.len());
2237 for (((l, r), attrs), (le, re)) in old_edges.into_iter().zip(old_endpoints) {
2238 if le == idx || re == idx {
2239 continue;
2240 }
2241 new_edges.insert(
2242 (
2243 if l > idx { l - 1 } else { l },
2244 if r > idx { r - 1 } else { r },
2245 ),
2246 attrs,
2247 );
2248 new_endpoints.push((
2249 if le > idx { le - 1 } else { le },
2250 if re > idx { re - 1 } else { re },
2251 ));
2252 }
2253 self.edges = new_edges;
2254 self.edge_index_endpoints = new_endpoints;
2255
2256 self.revision = self.revision.saturating_add(1);
2257 true
2258 }
2259
2260 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
2261 where
2262 I: IntoIterator<Item = &'a str>,
2263 {
2264 let remove_set: HashSet<&str> = nodes
2265 .into_iter()
2266 .filter(|node| self.nodes.contains_key(*node))
2267 .collect();
2268 if remove_set.is_empty() {
2269 return (0, 0);
2270 }
2271
2272 let old_node_count = self.nodes.len();
2273 let old_edge_count = self.edges.len();
2274
2275 let removed_mask: Vec<bool> = self
2281 .nodes
2282 .keys()
2283 .map(|k| remove_set.contains(k.as_str()))
2284 .collect();
2285 let mut remap: Vec<usize> = Vec::with_capacity(old_node_count);
2286 let mut next = 0usize;
2287 for &gone in &removed_mask {
2288 remap.push(next);
2289 if !gone {
2290 next += 1;
2291 }
2292 }
2293 let keep_edges: Vec<bool> = self
2294 .edge_index_endpoints
2295 .iter()
2296 .map(|&(l, r)| !removed_mask[l] && !removed_mask[r])
2297 .collect();
2298
2299 self.nodes
2300 .retain(|node, _| !remove_set.contains(node.as_str()));
2301 let mut i = 0usize;
2302 self.edges.retain(|_, _| {
2303 let k = keep_edges[i];
2304 i += 1;
2305 k
2306 });
2307 let mut j = 0usize;
2308 self.edge_index_endpoints.retain(|_| {
2309 let k = keep_edges[j];
2310 j += 1;
2311 k
2312 });
2313 for endpoints in &mut self.edge_index_endpoints {
2314 endpoints.0 = remap[endpoints.0];
2315 endpoints.1 = remap[endpoints.1];
2316 }
2317 self.edges = std::mem::take(&mut self.edges)
2319 .into_iter()
2320 .map(|((l, r), attrs)| ((remap[l], remap[r]), attrs))
2321 .collect();
2322 let old_rows = std::mem::take(&mut self.adj_indices);
2323 self.adj_indices = old_rows
2324 .into_iter()
2325 .enumerate()
2326 .filter(|(old_idx, _)| !removed_mask[*old_idx])
2327 .map(|(_, row)| {
2328 row.into_iter()
2329 .filter(|&nbr| !removed_mask[nbr])
2330 .map(|nbr| remap[nbr])
2331 .collect()
2332 })
2333 .collect();
2334
2335 let removed_nodes = old_node_count - self.nodes.len();
2336 let removed_edges = old_edge_count - self.edges.len();
2337 self.revision = self
2338 .revision
2339 .saturating_add(u64::try_from(removed_nodes).unwrap_or(u64::MAX));
2340 (removed_nodes, removed_edges)
2341 }
2342
2343 #[allow(dead_code)]
2346 #[must_use]
2349 pub fn edge_weights_all_int(&self, attr: &str) -> bool {
2350 if let Ok(guard) = self.all_int_cache.read()
2351 && let Some((rev, a, v)) = guard.as_ref()
2352 && *rev == self.revision
2353 && a == attr
2354 {
2355 return *v;
2356 }
2357 let v = self
2358 .edges
2359 .values()
2360 .all(|attrs| attrs.get(attr).is_none_or(fnx_runtime::CgseValue::is_int));
2361 if let Ok(mut guard) = self.all_int_cache.write() {
2362 *guard = Some((self.revision, attr.to_owned(), v));
2363 }
2364 v
2365 }
2366
2367 pub fn edges_ordered(&self) -> Vec<EdgeSnapshot> {
2368 let node_labels: Vec<&str> = self.nodes.keys().map(|s| s.as_str()).collect();
2374 let mut ordered = Vec::with_capacity(self.edges.len());
2375 let mut seen = vec![false; self.nodes.len()];
2376 for (u, row) in self.adj_indices.iter().enumerate() {
2377 for &v in row {
2378 if !seen[v] {
2379 let pair = if u <= v { (u, v) } else { (v, u) };
2380 if let Some(attrs) = self.edges.get(&pair) {
2381 ordered.push(EdgeSnapshot {
2382 left: node_labels[u].to_owned(),
2383 right: node_labels[v].to_owned(),
2384 attrs: attrs.clone(),
2385 });
2386 }
2387 }
2388 }
2389 seen[u] = true;
2390 }
2391
2392 ordered
2393 }
2394
2395 #[must_use]
2396 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, &AttrMap)> {
2397 let node_labels: Vec<&str> = self.nodes.keys().map(|s| s.as_str()).collect();
2399 let mut ordered = Vec::with_capacity(self.edges.len());
2400 let mut seen = vec![false; self.nodes.len()];
2401 for (u, row) in self.adj_indices.iter().enumerate() {
2402 for &v in row {
2403 if !seen[v] {
2404 let pair = if u <= v { (u, v) } else { (v, u) };
2405 if let Some(attrs) = self.edges.get(&pair) {
2406 ordered.push((node_labels[u], node_labels[v], attrs));
2407 }
2408 }
2409 }
2410 seen[u] = true;
2411 }
2412
2413 ordered
2414 }
2415
2416 #[must_use]
2427 pub fn weighted_size_int(&self, weight: &str) -> Option<i128> {
2428 let mut total: i128 = 0;
2429 for attrs in self.edges.values() {
2430 let value = match attrs.get(weight) {
2431 Some(CgseValue::Int(v)) => i128::from(*v),
2432 Some(_) => return None,
2433 None => 1,
2434 };
2435 total = total.checked_add(value)?;
2436 }
2437 Some(total)
2438 }
2439
2440 #[must_use]
2446 pub fn edges_ordered_indices(&self) -> Vec<(usize, usize)> {
2447 let node_count = self.adj_indices.len();
2458 let mut ordered = Vec::with_capacity(self.edges.len());
2459 let mut seen_source = vec![false; node_count];
2460 for (u, row) in self.adj_indices.iter().enumerate() {
2461 for &v in row {
2462 if !seen_source[v] {
2463 ordered.push((u, v));
2464 }
2465 }
2466 seen_source[u] = true;
2467 }
2468 if ordered.len() == self.edges.len() {
2469 return ordered;
2470 }
2471 let mut present: HashSet<(usize, usize)> = HashSet::with_capacity(self.edges.len());
2473 for &(l, r) in &self.edge_index_endpoints {
2474 present.insert(if l <= r { (l, r) } else { (r, l) });
2475 }
2476 let mut ordered = Vec::with_capacity(self.edges.len());
2477 let mut seen_pairs = HashSet::<(usize, usize)>::with_capacity(self.edges.len());
2478 for (u, row) in self.adj_indices.iter().enumerate() {
2479 for &v in row {
2480 let pair = if u <= v { (u, v) } else { (v, u) };
2481 if !seen_pairs.insert(pair) {
2482 continue;
2483 }
2484 if present.contains(&pair) {
2485 ordered.push((u, v));
2486 }
2487 }
2488 }
2489 if ordered.len() < self.edges.len() {
2490 for &(l, r) in self.edges.keys() {
2494 if seen_pairs.insert((l, r)) {
2495 ordered.push((l, r));
2496 }
2497 }
2498 }
2499 ordered
2500 }
2501
2502 #[must_use]
2503 pub fn edges_storage_order_borrowed(&self) -> Vec<(&str, &str, &AttrMap)> {
2504 self.edge_index_endpoints
2507 .iter()
2508 .zip(self.edges.values())
2509 .map(|(&(l, r), attrs)| {
2510 (
2511 self.nodes.get_index(l).expect("valid index").0.as_str(),
2512 self.nodes.get_index(r).expect("valid index").0.as_str(),
2513 attrs,
2514 )
2515 })
2516 .collect()
2517 }
2518
2519 pub fn edges_storage_order_iter(&self) -> impl Iterator<Item = (&str, &str, &AttrMap)> + '_ {
2520 self.edge_index_endpoints
2521 .iter()
2522 .zip(self.edges.values())
2523 .map(|(&(l, r), attrs)| {
2524 (
2525 self.nodes.get_index(l).expect("valid index").0.as_str(),
2526 self.nodes.get_index(r).expect("valid index").0.as_str(),
2527 attrs,
2528 )
2529 })
2530 }
2531
2532 #[must_use]
2533 pub fn snapshot(&self) -> GraphSnapshot {
2534 let node_attrs: BTreeMap<String, AttrMap> = self
2538 .nodes
2539 .iter()
2540 .filter(|(_, attrs)| !attrs.is_empty())
2541 .map(|(name, attrs)| (name.clone(), attrs.clone()))
2542 .collect();
2543 GraphSnapshot {
2544 mode: self.mode,
2545 nodes: self.nodes.keys().cloned().collect(),
2546 node_attrs,
2547 edges: self.edges_ordered(),
2548 }
2549 }
2550
2551 fn record_decision(
2552 &mut self,
2553 operation: &'static str,
2554 incompatibility_probability: f64,
2555 unknown_incompatible_feature: bool,
2556 evidence: Vec<EvidenceTerm>,
2557 ) -> DecisionAction {
2558 let action = self
2559 .runtime_policy
2560 .action_for(incompatibility_probability, unknown_incompatible_feature);
2561 self.runtime_policy.record(
2562 operation,
2563 action,
2564 incompatibility_probability,
2565 "argmin expected loss over {allow,full_validate,fail_closed}",
2566 evidence,
2567 );
2568 action
2569 }
2570}
2571
2572#[derive(Debug, Default)]
2579struct IntAdjCache(std::sync::RwLock<Option<(u64, Vec<Vec<usize>>)>>);
2580
2581impl Clone for IntAdjCache {
2582 fn clone(&self) -> Self {
2583 Self::default()
2584 }
2585}
2586
2587enum IntAdjEdgeDelta<'a> {
2591 Add {
2592 left: &'a str,
2593 right: &'a str,
2594 left_new: bool,
2595 right_new: bool,
2596 pair_new: bool,
2597 },
2598 Remove {
2599 left: &'a str,
2600 right: &'a str,
2601 pair_gone: bool,
2602 },
2603}
2604
2605mod multigraph_storage {
2608 use super::{AttrMap, CompatibilityMode, MultiEdgeSnapshot, MultiGraphSnapshot};
2609 use indexmap::{IndexMap, IndexSet};
2610
2611 #[cfg(test)]
2618 #[derive(Debug, Default)]
2619 pub struct MgIntStorageProto {
2620 pub node_names: Vec<String>,
2622 pub node_index: IndexMap<String, usize, rustc_hash::FxBuildHasher>,
2624 pub rows: Vec<IndexMap<usize, IndexSet<usize>>>,
2627 pub edges: IndexMap<(usize, usize), IndexMap<usize, AttrMap>, rustc_hash::FxBuildHasher>,
2629 pub edge_count: usize,
2630 }
2631
2632 #[cfg(test)]
2633 impl MgIntStorageProto {
2634 #[must_use]
2635 pub fn new() -> Self {
2636 Self::default()
2637 }
2638
2639 fn ensure_node(&mut self, name: &str) -> usize {
2640 if let Some(&idx) = self.node_index.get(name) {
2641 return idx;
2642 }
2643 let idx = self.node_names.len();
2644 self.node_names.push(name.to_owned());
2645 self.node_index.insert(name.to_owned(), idx);
2646 self.rows.push(IndexMap::new());
2647 idx
2648 }
2649
2650 pub fn add_keyed_edge(&mut self, left: &str, right: &str, key: usize, attrs: AttrMap) {
2654 let l = self.ensure_node(left);
2655 let r = if left == right {
2656 l
2657 } else {
2658 self.ensure_node(right)
2659 };
2660 let pair = (l.min(r), l.max(r));
2661 let bucket = self.edges.entry(pair).or_default();
2662 if !bucket.contains_key(&key) {
2663 self.edge_count += 1;
2664 }
2665 bucket.entry(key).or_default().extend(attrs);
2666 self.rows[l].entry(r).or_default().insert(key);
2667 if l != r {
2668 self.rows[r].entry(l).or_default().insert(key);
2669 }
2670 }
2671
2672 #[must_use]
2677 pub fn bulk_load_int_prefix<I>(node_count: usize, edges: I) -> Self
2678 where
2679 I: IntoIterator<Item = (usize, usize, usize)>,
2680 {
2681 let iterator = edges.into_iter();
2682 let (lower_bound, _) = iterator.size_hint();
2683 let mut proto = Self::new();
2684 proto.node_names.reserve(node_count);
2685 proto.node_index.reserve(node_count);
2686 proto.rows.reserve(node_count);
2687 proto.edges.reserve(lower_bound);
2688 for node in 0..node_count {
2689 let name = node.to_string();
2690 proto.node_names.push(name.clone());
2691 proto.node_index.insert(name, node);
2692 proto.rows.push(IndexMap::new());
2693 }
2694 for (left_idx, right_idx, key) in iterator {
2695 debug_assert!(left_idx < node_count);
2696 debug_assert!(right_idx < node_count);
2697 let pair = (left_idx.min(right_idx), left_idx.max(right_idx));
2698 proto
2699 .edges
2700 .entry(pair)
2701 .or_default()
2702 .insert(key, AttrMap::new());
2703 proto.rows[left_idx]
2704 .entry(right_idx)
2705 .or_default()
2706 .insert(key);
2707 if left_idx != right_idx {
2708 proto.rows[right_idx]
2709 .entry(left_idx)
2710 .or_default()
2711 .insert(key);
2712 }
2713 proto.edge_count += 1;
2714 }
2715 proto
2716 }
2717
2718 #[must_use]
2720 pub fn neighbor_names(&self, i: usize) -> Vec<&str> {
2721 self.rows[i]
2722 .keys()
2723 .map(|&j| self.node_names[j].as_str())
2724 .collect()
2725 }
2726
2727 #[must_use]
2729 pub fn key_order(&self, left: &str, right: &str) -> Vec<usize> {
2730 let (Some(&l), Some(&r)) = (self.node_index.get(left), self.node_index.get(right))
2731 else {
2732 return Vec::new();
2733 };
2734 self.edges
2735 .get(&(l.min(r), l.max(r)))
2736 .map(|bucket| bucket.keys().copied().collect())
2737 .unwrap_or_default()
2738 }
2739
2740 pub fn remove_edge(&mut self, left: &str, right: &str, key: Option<usize>) -> bool {
2746 let (Some(&l), Some(&r)) = (self.node_index.get(left), self.node_index.get(right))
2747 else {
2748 return false;
2749 };
2750 let pair = (l.min(r), l.max(r));
2751 let Some(bucket) = self.edges.get_mut(&pair) else {
2752 return false;
2753 };
2754 let Some(removal_key) = key.or_else(|| bucket.keys().next_back().copied()) else {
2755 return false;
2756 };
2757 if bucket.shift_remove(&removal_key).is_none() {
2758 return false;
2759 }
2760 self.edge_count -= 1;
2761 let pair_gone = bucket.is_empty();
2762 if pair_gone {
2763 self.edges.swap_remove(&pair);
2764 }
2765 let mut drop_cell = |u: usize, v: usize| {
2766 let mut cell_empty = false;
2767 if let Some(keys) = self.rows[u].get_mut(&v) {
2768 keys.shift_remove(&removal_key);
2769 cell_empty = keys.is_empty();
2770 }
2771 if cell_empty {
2772 self.rows[u].shift_remove(&v);
2773 }
2774 };
2775 drop_cell(l, r);
2776 if l != r {
2777 drop_cell(r, l);
2778 }
2779 true
2780 }
2781
2782 pub fn remove_node(&mut self, name: &str) -> bool {
2790 let Some(&idx) = self.node_index.get(name) else {
2791 return false;
2792 };
2793 self.node_names.remove(idx);
2794 self.node_index.shift_remove(name);
2795 for stored in self.node_index.values_mut() {
2796 if *stored > idx {
2797 *stored -= 1;
2798 }
2799 }
2800 self.rows.remove(idx);
2801 for row in &mut self.rows {
2802 let old = std::mem::take(row);
2803 for (j, keys) in old {
2804 if j == idx {
2805 continue;
2806 }
2807 row.insert(if j > idx { j - 1 } else { j }, keys);
2808 }
2809 }
2810 let old_edges = std::mem::take(&mut self.edges);
2811 let mut removed_cells = 0usize;
2812 for ((a, b), bucket) in old_edges {
2813 if a == idx || b == idx {
2814 removed_cells += bucket.len();
2815 continue;
2816 }
2817 self.edges.insert(
2818 (
2819 if a > idx { a - 1 } else { a },
2820 if b > idx { b - 1 } else { b },
2821 ),
2822 bucket,
2823 );
2824 }
2825 self.edge_count -= removed_cells;
2826 true
2827 }
2828 }
2829
2830 #[derive(Debug, Clone)]
2836 pub enum CompactKeys {
2837 One(usize),
2838 Many(IndexSet<usize>),
2839 }
2840
2841 impl CompactKeys {
2842 pub fn insert(&mut self, key: usize) -> bool {
2844 match self {
2845 Self::One(existing) => {
2846 if *existing == key {
2847 return false;
2848 }
2849 let mut set = IndexSet::with_capacity(2);
2850 set.insert(*existing);
2851 set.insert(key);
2852 *self = Self::Many(set);
2853 true
2854 }
2855 Self::Many(set) => set.insert(key),
2856 }
2857 }
2858
2859 #[cfg(test)]
2860 #[must_use]
2861 pub fn order(&self) -> Vec<usize> {
2862 match self {
2863 Self::One(key) => vec![*key],
2864 Self::Many(set) => set.iter().copied().collect(),
2865 }
2866 }
2867
2868 #[must_use]
2871 pub fn len(&self) -> usize {
2872 match self {
2873 Self::One(_) => 1,
2874 Self::Many(set) => set.len(),
2875 }
2876 }
2877
2878 pub fn shift_remove(&mut self, key: usize) -> (bool, bool) {
2882 match self {
2883 Self::One(existing) => {
2884 let removed = *existing == key;
2885 (removed, removed)
2886 }
2887 Self::Many(set) => {
2888 let removed = set.shift_remove(&key);
2889 (removed, set.is_empty())
2890 }
2891 }
2892 }
2893 }
2894
2895 #[derive(Debug, Clone)]
2900 pub enum CompactBucket {
2901 One(usize, AttrMap),
2902 Many(IndexMap<usize, AttrMap>),
2903 }
2904
2905 impl CompactBucket {
2906 pub fn merge(&mut self, key: usize, attrs: AttrMap) -> bool {
2909 match self {
2910 Self::One(existing, existing_attrs) => {
2911 if *existing == key {
2912 existing_attrs.extend(attrs);
2913 return false;
2914 }
2915 let mut map = IndexMap::with_capacity(2);
2916 map.insert(*existing, std::mem::take(existing_attrs));
2917 map.insert(key, attrs);
2918 *self = Self::Many(map);
2919 true
2920 }
2921 Self::Many(map) => {
2922 let is_new = !map.contains_key(&key);
2923 map.entry(key).or_default().extend(attrs);
2924 is_new
2925 }
2926 }
2927 }
2928
2929 #[must_use]
2930 pub fn key_order(&self) -> Vec<usize> {
2931 match self {
2932 Self::One(key, _) => vec![*key],
2933 Self::Many(map) => map.keys().copied().collect(),
2934 }
2935 }
2936
2937 #[must_use]
2938 pub fn attrs_for(&self, key: usize) -> Option<&AttrMap> {
2939 match self {
2940 Self::One(existing, attrs) => (*existing == key).then_some(attrs),
2941 Self::Many(map) => map.get(&key),
2942 }
2943 }
2944
2945 #[must_use]
2946 pub fn contains_key(&self, key: usize) -> bool {
2947 self.attrs_for(key).is_some()
2948 }
2949
2950 pub fn replace_attrs(&mut self, key: usize, attrs: AttrMap) -> bool {
2954 match self {
2955 Self::One(existing, existing_attrs) => {
2956 if *existing == key {
2957 *existing_attrs = attrs;
2958 true
2959 } else {
2960 false
2961 }
2962 }
2963 Self::Many(map) => {
2964 if let Some(slot) = map.get_mut(&key) {
2965 *slot = attrs;
2966 true
2967 } else {
2968 false
2969 }
2970 }
2971 }
2972 }
2973
2974 #[must_use]
2976 pub fn len(&self) -> usize {
2977 match self {
2978 Self::One(..) => 1,
2979 Self::Many(map) => map.len(),
2980 }
2981 }
2982
2983 #[must_use]
2984 pub fn is_empty(&self) -> bool {
2985 self.len() == 0
2986 }
2987
2988 #[must_use]
2991 pub fn last_key(&self) -> Option<usize> {
2992 match self {
2993 Self::One(key, _) => Some(*key),
2994 Self::Many(map) => map.keys().next_back().copied(),
2995 }
2996 }
2997
2998 pub fn shift_remove(&mut self, key: usize) -> (bool, bool) {
3001 match self {
3002 Self::One(existing, _) => {
3003 let removed = *existing == key;
3004 (removed, removed)
3005 }
3006 Self::Many(map) => {
3007 let removed = map.shift_remove(&key).is_some();
3008 (removed, map.is_empty())
3009 }
3010 }
3011 }
3012
3013 pub fn iter(&self) -> CompactBucketIter<'_> {
3016 match self {
3017 Self::One(key, attrs) => CompactBucketIter::One(std::iter::once((*key, attrs))),
3018 Self::Many(map) => CompactBucketIter::Many(map.iter()),
3019 }
3020 }
3021
3022 pub fn keys(&self) -> CompactBucketKeys<'_> {
3026 match self {
3027 Self::One(key, _) => CompactBucketKeys::One(Some(key)),
3028 Self::Many(map) => CompactBucketKeys::Many(map.keys()),
3029 }
3030 }
3031
3032 pub fn values(&self) -> CompactBucketValues<'_> {
3033 match self {
3034 Self::One(_, attrs) => CompactBucketValues::One(Some(attrs)),
3035 Self::Many(map) => CompactBucketValues::Many(map.values()),
3036 }
3037 }
3038 }
3039
3040 pub enum CompactBucketIter<'a> {
3043 One(std::iter::Once<(usize, &'a AttrMap)>),
3044 Many(indexmap::map::Iter<'a, usize, AttrMap>),
3045 }
3046
3047 impl<'a> Iterator for CompactBucketIter<'a> {
3048 type Item = (usize, &'a AttrMap);
3049
3050 fn next(&mut self) -> Option<Self::Item> {
3051 match self {
3052 Self::One(it) => it.next(),
3053 Self::Many(it) => it.next().map(|(key, attrs)| (*key, attrs)),
3054 }
3055 }
3056 }
3057
3058 pub enum CompactBucketKeys<'a> {
3062 One(Option<&'a usize>),
3063 Many(indexmap::map::Keys<'a, usize, AttrMap>),
3064 }
3065
3066 impl<'a> Iterator for CompactBucketKeys<'a> {
3067 type Item = &'a usize;
3068
3069 fn next(&mut self) -> Option<Self::Item> {
3070 match self {
3071 Self::One(opt) => opt.take(),
3072 Self::Many(keys) => keys.next(),
3073 }
3074 }
3075 }
3076
3077 pub enum CompactBucketValues<'a> {
3078 One(Option<&'a AttrMap>),
3079 Many(indexmap::map::Values<'a, usize, AttrMap>),
3080 }
3081
3082 impl<'a> Iterator for CompactBucketValues<'a> {
3083 type Item = &'a AttrMap;
3084
3085 fn next(&mut self) -> Option<Self::Item> {
3086 match self {
3087 Self::One(opt) => opt.take(),
3088 Self::Many(values) => values.next(),
3089 }
3090 }
3091 }
3092
3093 #[cfg(test)]
3099 #[derive(Debug, Default)]
3100 pub struct MgIntStorageProtoCompact {
3101 pub node_names: Vec<String>,
3102 pub node_index: IndexMap<String, usize, rustc_hash::FxBuildHasher>,
3103 pub rows: Vec<IndexMap<usize, CompactKeys>>,
3104 pub edges: IndexMap<(usize, usize), CompactBucket, rustc_hash::FxBuildHasher>,
3105 pub edge_count: usize,
3106 }
3107
3108 #[cfg(test)]
3109 impl MgIntStorageProtoCompact {
3110 #[must_use]
3111 pub fn new() -> Self {
3112 Self::default()
3113 }
3114
3115 fn ensure_node(&mut self, name: &str) -> usize {
3116 if let Some(&idx) = self.node_index.get(name) {
3117 return idx;
3118 }
3119 let idx = self.node_names.len();
3120 self.node_names.push(name.to_owned());
3121 self.node_index.insert(name.to_owned(), idx);
3122 self.rows.push(IndexMap::new());
3123 idx
3124 }
3125
3126 pub fn add_keyed_edge(
3128 &mut self,
3129 left: &str,
3130 right: &str,
3131 key: usize,
3132 attrs: AttrMap,
3133 ) -> bool {
3134 let l = self.ensure_node(left);
3135 let r = if left == right {
3136 l
3137 } else {
3138 self.ensure_node(right)
3139 };
3140 let pair = (l.min(r), l.max(r));
3141 let is_new = match self.edges.entry(pair) {
3142 indexmap::map::Entry::Occupied(mut bucket) => bucket.get_mut().merge(key, attrs),
3143 indexmap::map::Entry::Vacant(slot) => {
3144 slot.insert(CompactBucket::One(key, attrs));
3145 true
3146 }
3147 };
3148 if is_new {
3149 self.edge_count += 1;
3150 }
3151 match self.rows[l].entry(r) {
3152 indexmap::map::Entry::Occupied(mut cell) => {
3153 let _ = cell.get_mut().insert(key);
3154 }
3155 indexmap::map::Entry::Vacant(slot) => {
3156 slot.insert(CompactKeys::One(key));
3157 }
3158 }
3159 if l != r {
3160 match self.rows[r].entry(l) {
3161 indexmap::map::Entry::Occupied(mut cell) => {
3162 let _ = cell.get_mut().insert(key);
3163 }
3164 indexmap::map::Entry::Vacant(slot) => {
3165 slot.insert(CompactKeys::One(key));
3166 }
3167 }
3168 }
3169 is_new
3170 }
3171
3172 #[cfg(test)]
3174 #[must_use]
3175 pub fn bulk_load_int_prefix<I>(node_count: usize, edges: I) -> Self
3176 where
3177 I: IntoIterator<Item = (usize, usize, usize)>,
3178 {
3179 let iterator = edges.into_iter();
3180 let (lower_bound, _) = iterator.size_hint();
3181 let mut proto = Self::new();
3182 proto.node_names.reserve(node_count);
3183 proto.node_index.reserve(node_count);
3184 proto.rows.reserve(node_count);
3185 proto.edges.reserve(lower_bound);
3186 for node in 0..node_count {
3187 let name = node.to_string();
3188 proto.node_names.push(name.clone());
3189 proto.node_index.insert(name, node);
3190 proto.rows.push(IndexMap::new());
3191 }
3192 for (left_idx, right_idx, key) in iterator {
3193 debug_assert!(left_idx < node_count);
3194 debug_assert!(right_idx < node_count);
3195 let pair = (left_idx.min(right_idx), left_idx.max(right_idx));
3196 match proto.edges.entry(pair) {
3197 indexmap::map::Entry::Occupied(mut bucket) => {
3198 let _ = bucket.get_mut().merge(key, AttrMap::new());
3199 }
3200 indexmap::map::Entry::Vacant(slot) => {
3201 slot.insert(CompactBucket::One(key, AttrMap::new()));
3202 }
3203 }
3204 match proto.rows[left_idx].entry(right_idx) {
3205 indexmap::map::Entry::Occupied(mut cell) => {
3206 let _ = cell.get_mut().insert(key);
3207 }
3208 indexmap::map::Entry::Vacant(slot) => {
3209 slot.insert(CompactKeys::One(key));
3210 }
3211 }
3212 if left_idx != right_idx {
3213 match proto.rows[right_idx].entry(left_idx) {
3214 indexmap::map::Entry::Occupied(mut cell) => {
3215 let _ = cell.get_mut().insert(key);
3216 }
3217 indexmap::map::Entry::Vacant(slot) => {
3218 slot.insert(CompactKeys::One(key));
3219 }
3220 }
3221 }
3222 proto.edge_count += 1;
3223 }
3224 proto
3225 }
3226
3227 #[must_use]
3229 pub fn neighbor_names(&self, i: usize) -> Vec<&str> {
3230 self.rows[i]
3231 .keys()
3232 .map(|&j| self.node_names[j].as_str())
3233 .collect()
3234 }
3235
3236 #[must_use]
3238 pub fn key_order(&self, left: &str, right: &str) -> Vec<usize> {
3239 let (Some(&l), Some(&r)) = (self.node_index.get(left), self.node_index.get(right))
3240 else {
3241 return Vec::new();
3242 };
3243 self.edges
3244 .get(&(l.min(r), l.max(r)))
3245 .map(CompactBucket::key_order)
3246 .unwrap_or_default()
3247 }
3248 }
3249
3250 #[derive(Debug, Clone, Default)]
3260 pub struct MgSlabStorage {
3261 pub node_order: IndexMap<String, usize, rustc_hash::FxBuildHasher>,
3263 pub slot_names: Vec<Option<String>>,
3265 pub node_attrs: Vec<AttrMap>,
3270 pub free_slots: Vec<usize>,
3272 pub rows: Vec<IndexMap<usize, CompactKeys>>,
3274 pub edges: IndexMap<(usize, usize), CompactBucket, rustc_hash::FxBuildHasher>,
3277 pub edge_count: usize,
3278 }
3279
3280 impl MgSlabStorage {
3281 #[must_use]
3282 pub fn new() -> Self {
3283 Self::default()
3284 }
3285
3286 fn ensure_node(&mut self, name: &str) -> usize {
3287 if let Some(&slot) = self.node_order.get(name) {
3288 return slot;
3289 }
3290 let slot = if let Some(reused) = self.free_slots.pop() {
3291 debug_assert!(self.slot_names[reused].is_none());
3292 debug_assert!(self.rows[reused].is_empty());
3293 self.slot_names[reused] = Some(name.to_owned());
3294 self.node_attrs[reused] = AttrMap::new();
3295 reused
3296 } else {
3297 self.slot_names.push(Some(name.to_owned()));
3298 self.rows.push(IndexMap::new());
3299 self.node_attrs.push(AttrMap::new());
3300 self.slot_names.len() - 1
3301 };
3302 self.node_order.insert(name.to_owned(), slot);
3303 slot
3304 }
3305
3306 pub fn add_keyed_edge(
3308 &mut self,
3309 left: &str,
3310 right: &str,
3311 key: usize,
3312 attrs: AttrMap,
3313 ) -> bool {
3314 let l = self.ensure_node(left);
3315 let r = if left == right {
3316 l
3317 } else {
3318 self.ensure_node(right)
3319 };
3320 self.add_keyed_edge_by_pair(l, r, key, attrs)
3321 }
3322
3323 pub fn add_keyed_edge_by_pair(
3329 &mut self,
3330 l: usize,
3331 r: usize,
3332 key: usize,
3333 attrs: AttrMap,
3334 ) -> bool {
3335 let pair = (l.min(r), l.max(r));
3336 let is_new = match self.edges.entry(pair) {
3337 indexmap::map::Entry::Occupied(mut bucket) => bucket.get_mut().merge(key, attrs),
3338 indexmap::map::Entry::Vacant(slot) => {
3339 slot.insert(CompactBucket::One(key, attrs));
3340 true
3341 }
3342 };
3343 if is_new {
3344 self.edge_count += 1;
3345 }
3346 match self.rows[l].entry(r) {
3347 indexmap::map::Entry::Occupied(mut cell) => {
3348 let _ = cell.get_mut().insert(key);
3349 }
3350 indexmap::map::Entry::Vacant(slot) => {
3351 slot.insert(CompactKeys::One(key));
3352 }
3353 }
3354 if l != r {
3355 match self.rows[r].entry(l) {
3356 indexmap::map::Entry::Occupied(mut cell) => {
3357 let _ = cell.get_mut().insert(key);
3358 }
3359 indexmap::map::Entry::Vacant(slot) => {
3360 slot.insert(CompactKeys::One(key));
3361 }
3362 }
3363 }
3364 is_new
3365 }
3366
3367 #[cfg(test)]
3370 #[must_use]
3371 pub fn bulk_load_int_prefix<I>(node_count: usize, edges: I) -> Self
3372 where
3373 I: IntoIterator<Item = (usize, usize, usize)>,
3374 {
3375 let iterator = edges.into_iter();
3376 let (lower_bound, _) = iterator.size_hint();
3377 let mut proto = Self::new();
3378 proto.node_order.reserve(node_count);
3379 proto.slot_names.reserve(node_count);
3380 proto.rows.reserve(node_count);
3381 proto.edges.reserve(lower_bound);
3382 proto.node_attrs.reserve(node_count);
3383 for node in 0..node_count {
3384 let name = node.to_string();
3385 proto.node_order.insert(name.clone(), node);
3386 proto.slot_names.push(Some(name));
3387 proto.rows.push(IndexMap::new());
3388 proto.node_attrs.push(AttrMap::new());
3389 }
3390 for (left_slot, right_slot, key) in iterator {
3391 debug_assert!(left_slot < node_count);
3392 debug_assert!(right_slot < node_count);
3393 let pair = (left_slot.min(right_slot), left_slot.max(right_slot));
3394 match proto.edges.entry(pair) {
3395 indexmap::map::Entry::Occupied(mut bucket) => {
3396 let _ = bucket.get_mut().merge(key, AttrMap::new());
3397 }
3398 indexmap::map::Entry::Vacant(slot) => {
3399 slot.insert(CompactBucket::One(key, AttrMap::new()));
3400 }
3401 }
3402 match proto.rows[left_slot].entry(right_slot) {
3403 indexmap::map::Entry::Occupied(mut cell) => {
3404 let _ = cell.get_mut().insert(key);
3405 }
3406 indexmap::map::Entry::Vacant(slot) => {
3407 slot.insert(CompactKeys::One(key));
3408 }
3409 }
3410 if left_slot != right_slot {
3411 match proto.rows[right_slot].entry(left_slot) {
3412 indexmap::map::Entry::Occupied(mut cell) => {
3413 let _ = cell.get_mut().insert(key);
3414 }
3415 indexmap::map::Entry::Vacant(slot) => {
3416 slot.insert(CompactKeys::One(key));
3417 }
3418 }
3419 }
3420 proto.edge_count += 1;
3421 }
3422 proto
3423 }
3424
3425 pub fn remove_edge(&mut self, left: &str, right: &str, key: Option<usize>) -> bool {
3427 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
3428 else {
3429 return false;
3430 };
3431 let pair = (l.min(r), l.max(r));
3432 let Some(bucket) = self.edges.get_mut(&pair) else {
3433 return false;
3434 };
3435 let Some(removal_key) = key.or_else(|| bucket.last_key()) else {
3436 return false;
3437 };
3438 let (removed, bucket_empty) = bucket.shift_remove(removal_key);
3439 if !removed {
3440 return false;
3441 }
3442 self.edge_count -= 1;
3443 if bucket_empty {
3444 self.edges.swap_remove(&pair);
3445 }
3446 let mut drop_cell = |u: usize, v: usize| {
3447 let mut cell_empty = false;
3448 if let Some(keys) = self.rows[u].get_mut(&v) {
3449 let (cell_removed, now_empty) = keys.shift_remove(removal_key);
3450 debug_assert!(cell_removed);
3451 cell_empty = now_empty;
3452 }
3453 if cell_empty {
3454 self.rows[u].shift_remove(&v);
3455 }
3456 };
3457 drop_cell(l, r);
3458 if l != r {
3459 drop_cell(r, l);
3460 }
3461 true
3462 }
3463
3464 pub fn remove_node(&mut self, name: &str) -> bool {
3467 let Some(&slot) = self.node_order.get(name) else {
3468 return false;
3469 };
3470 let neighbors: Vec<usize> = self.rows[slot].keys().copied().collect();
3471 for nbr in neighbors {
3472 if nbr != slot {
3473 self.rows[nbr].shift_remove(&slot);
3474 }
3475 if let Some(bucket) = self.edges.swap_remove(&(slot.min(nbr), slot.max(nbr))) {
3476 self.edge_count -= bucket.len();
3477 }
3478 }
3479 self.rows[slot] = IndexMap::new();
3480 self.node_order.shift_remove(name);
3481 self.slot_names[slot] = None;
3482 self.node_attrs[slot] = AttrMap::new();
3483 self.free_slots.push(slot);
3484 true
3485 }
3486
3487 pub fn set_node_attrs(&mut self, name: &str, attrs: AttrMap) -> (bool, bool, usize) {
3490 let existed = self.node_order.contains_key(name);
3491 let changed = !existed
3492 || attrs.iter().any(|(key, value)| {
3493 self.node_attrs_for(name).and_then(|a| a.get(key)) != Some(value)
3494 });
3495 let slot = self.ensure_node(name);
3496 self.node_attrs[slot].extend(attrs);
3497 (existed, changed, self.node_attrs[slot].len())
3498 }
3499
3500 pub fn replace_node_attrs(&mut self, name: &str, attrs: AttrMap) {
3502 if let Some(&slot) = self.node_order.get(name) {
3503 self.node_attrs[slot] = attrs;
3504 }
3505 }
3506
3507 #[cfg(test)]
3510 #[must_use]
3511 pub fn neighbor_names(&self, pos: usize) -> Vec<&str> {
3512 let Some((_, &slot)) = self.node_order.get_index(pos) else {
3513 return Vec::new();
3514 };
3515 self.rows[slot]
3516 .keys()
3517 .map(|&s| {
3518 self.slot_names[s]
3519 .as_deref()
3520 .expect("row references a live slot")
3521 })
3522 .collect()
3523 }
3524
3525 #[must_use]
3528 pub fn neighbors_by_name(&self, name: &str) -> Option<Vec<&str>> {
3529 let &slot = self.node_order.get(name)?;
3530 Some(
3531 self.rows[slot]
3532 .keys()
3533 .map(|&s| {
3534 self.slot_names[s]
3535 .as_deref()
3536 .expect("row references a live slot")
3537 })
3538 .collect(),
3539 )
3540 }
3541
3542 #[must_use]
3546 pub fn degree(&self, name: &str) -> usize {
3547 let Some(&slot) = self.node_order.get(name) else {
3548 return 0;
3549 };
3550 let mut deg = 0;
3551 for (&nbr_slot, keys) in &self.rows[slot] {
3552 let count = keys.len();
3553 if nbr_slot == slot {
3554 deg += count * 2;
3555 } else {
3556 deg += count;
3557 }
3558 }
3559 deg
3560 }
3561
3562 #[must_use]
3563 pub fn number_of_selfloops(&self) -> usize {
3564 self.edges
3565 .iter()
3566 .filter(|((left, right), _)| left == right)
3567 .map(|(_, bucket)| bucket.len())
3568 .sum()
3569 }
3570
3571 #[must_use]
3573 pub fn has_node(&self, name: &str) -> bool {
3574 self.node_order.contains_key(name)
3575 }
3576
3577 #[must_use]
3580 pub fn has_edge(&self, left: &str, right: &str) -> bool {
3581 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
3582 else {
3583 return false;
3584 };
3585 self.has_edge_by_pair(l, r)
3586 }
3587
3588 #[must_use]
3599 pub fn has_edge_by_pair(&self, l: usize, r: usize) -> bool {
3600 self.edges
3601 .get(&(l.min(r), l.max(r)))
3602 .is_some_and(|bucket| !bucket.is_empty())
3603 }
3604
3605 #[must_use]
3607 pub fn node_slot(&self, name: &str) -> Option<usize> {
3608 self.node_order.get(name).copied()
3609 }
3610
3611 #[must_use]
3622 pub fn slot_at_position(&self, position: usize) -> Option<usize> {
3623 self.node_order.get_index(position).map(|(_, slot)| *slot)
3624 }
3625
3626 #[must_use]
3629 pub fn is_isolate(&self, name: &str) -> bool {
3630 self.node_order
3631 .get(name)
3632 .is_some_and(|&slot| self.rows[slot].is_empty())
3633 }
3634
3635 #[must_use]
3636 pub fn isolates(&self) -> Vec<String> {
3637 self.node_order
3638 .iter()
3639 .filter(|(_, slot)| self.rows[**slot].is_empty())
3640 .map(|(name, _)| name.clone())
3641 .collect()
3642 }
3643
3644 #[must_use]
3645 pub fn number_of_isolates(&self) -> usize {
3646 self.node_order
3647 .values()
3648 .filter(|&&slot| self.rows[slot].is_empty())
3649 .count()
3650 }
3651
3652 #[must_use]
3657 pub fn weighted_size_int(&self, weight: &str) -> Option<i128> {
3658 let mut total: i128 = 0;
3659 for bucket in self.edges.values() {
3660 for (_key, attrs) in bucket.iter() {
3661 let value = match attrs.get(weight) {
3662 Some(fnx_runtime::CgseValue::Int(v)) => i128::from(*v),
3663 Some(_) => return None,
3664 None => 1,
3665 };
3666 total = total.checked_add(value)?;
3667 }
3668 }
3669 Some(total)
3670 }
3671
3672 #[must_use]
3674 pub fn key_order(&self, left: &str, right: &str) -> Vec<usize> {
3675 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
3676 else {
3677 return Vec::new();
3678 };
3679 self.edges
3680 .get(&(l.min(r), l.max(r)))
3681 .map(CompactBucket::key_order)
3682 .unwrap_or_default()
3683 }
3684
3685 #[must_use]
3686 pub fn edge_keys(&self, left: &str, right: &str) -> Option<Vec<usize>> {
3687 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
3688 else {
3689 return None;
3690 };
3691 self.edges
3692 .get(&(l.min(r), l.max(r)))
3693 .map(CompactBucket::key_order)
3694 }
3695
3696 pub fn edge_keys_iter(&self, left: &str, right: &str) -> Option<CompactBucketKeys<'_>> {
3697 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
3698 else {
3699 return None;
3700 };
3701 self.edges
3702 .get(&(l.min(r), l.max(r)))
3703 .map(CompactBucket::keys)
3704 }
3705
3706 pub fn edge_attr_values(&self, left: &str, right: &str) -> Option<CompactBucketValues<'_>> {
3707 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
3708 else {
3709 return None;
3710 };
3711 self.edges
3712 .get(&(l.min(r), l.max(r)))
3713 .map(CompactBucket::values)
3714 }
3715
3716 #[must_use]
3720 pub fn edge_attr_count_by_pair(&self, l: usize, r: usize) -> usize {
3721 self.edges
3722 .get(&(l.min(r), l.max(r)))
3723 .map_or(0, CompactBucket::len)
3724 }
3725
3726 #[must_use]
3730 pub fn next_edge_key_by_pair(&self, l: usize, r: usize) -> usize {
3731 let Some(bucket) = self.edges.get(&(l.min(r), l.max(r))) else {
3732 return 0;
3733 };
3734 let mut key = bucket.len();
3735 while bucket.contains_key(key) {
3736 key += 1;
3737 }
3738 key
3739 }
3740
3741 pub fn replace_edge_attrs(
3743 &mut self,
3744 left: &str,
3745 right: &str,
3746 key: usize,
3747 attrs: AttrMap,
3748 ) -> bool {
3749 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
3750 else {
3751 return false;
3752 };
3753 self.edges
3754 .get_mut(&(l.min(r), l.max(r)))
3755 .is_some_and(|bucket| bucket.replace_attrs(key, attrs))
3756 }
3757
3758 #[must_use]
3760 pub fn edge_attrs(&self, left: &str, right: &str, key: usize) -> Option<&AttrMap> {
3761 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
3762 else {
3763 return None;
3764 };
3765 self.edge_attrs_by_pair(l, r, key)
3766 }
3767
3768 #[must_use]
3770 pub fn edge_attrs_by_pair(&self, l: usize, r: usize, key: usize) -> Option<&AttrMap> {
3771 self.edges
3772 .get(&(l.min(r), l.max(r)))
3773 .and_then(|bucket| bucket.attrs_for(key))
3774 }
3775
3776 #[must_use]
3778 pub fn node_attrs_for(&self, name: &str) -> Option<&AttrMap> {
3779 let &slot = self.node_order.get(name)?;
3780 Some(&self.node_attrs[slot])
3781 }
3782
3783 #[must_use]
3784 pub fn get_node_index(&self, name: &str) -> Option<usize> {
3785 self.node_order.get_index_of(name)
3786 }
3787
3788 #[must_use]
3789 pub fn get_node_name(&self, index: usize) -> Option<&str> {
3790 self.node_order
3791 .get_index(index)
3792 .map(|(name, _)| name.as_str())
3793 }
3794
3795 #[must_use]
3796 pub fn node_index_matches_int(&self, index: usize) -> bool {
3797 self.node_order
3798 .get_index(index)
3799 .is_some_and(|(name, _)| name.parse::<usize>() == Ok(index))
3800 }
3801
3802 #[must_use]
3803 pub fn build_int_adjacency(&self) -> Vec<Vec<usize>> {
3804 let slot_to_position = self.slot_positions();
3805 self.node_order
3806 .values()
3807 .map(|&slot| {
3808 self.rows[slot]
3809 .keys()
3810 .map(|&neighbor_slot| slot_to_position[neighbor_slot])
3811 .collect()
3812 })
3813 .collect()
3814 }
3815
3816 fn slot_positions(&self) -> Vec<usize> {
3819 let mut pos = vec![usize::MAX; self.slot_names.len()];
3820 for (p, (_, &slot)) in self.node_order.iter().enumerate() {
3821 pos[slot] = p;
3822 }
3823 pos
3824 }
3825
3826 #[must_use]
3833 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, usize, &AttrMap)> {
3834 let mut ordered = Vec::with_capacity(self.edge_count);
3835 let mut seen: std::collections::HashSet<(usize, usize, usize)> =
3836 std::collections::HashSet::with_capacity(self.edge_count);
3837 for (u_name, &u_slot) in &self.node_order {
3838 for &v_slot in self.rows[u_slot].keys() {
3839 let pair = (u_slot.min(v_slot), u_slot.max(v_slot));
3840 let Some(bucket) = self.edges.get(&pair) else {
3841 continue;
3842 };
3843 for (key, attrs) in bucket.iter() {
3844 if !seen.insert((pair.0, pair.1, key)) {
3845 continue;
3846 }
3847 let v_name = self.slot_names[v_slot]
3848 .as_deref()
3849 .expect("row references a live slot");
3850 let (left, right) = if u_name.as_str() <= v_name {
3851 (u_name.as_str(), v_name)
3852 } else {
3853 (v_name, u_name.as_str())
3854 };
3855 ordered.push((left, right, key, attrs));
3856 }
3857 }
3858 }
3859 ordered
3860 }
3861
3862 #[must_use]
3863 pub fn snapshot(&self, mode: CompatibilityMode) -> MultiGraphSnapshot {
3864 let node_attrs = self
3865 .node_order
3866 .iter()
3867 .filter_map(|(name, &slot)| {
3868 let attrs = &self.node_attrs[slot];
3869 (!attrs.is_empty()).then(|| (name.clone(), attrs.clone()))
3870 })
3871 .collect();
3872 let edges = self
3873 .edges_ordered_borrowed()
3874 .into_iter()
3875 .map(|(left, right, key, attrs)| MultiEdgeSnapshot {
3876 left: left.to_owned(),
3877 right: right.to_owned(),
3878 key,
3879 attrs: attrs.clone(),
3880 })
3881 .collect();
3882 MultiGraphSnapshot {
3883 mode,
3884 nodes: self.node_order.keys().cloned().collect(),
3885 node_attrs,
3886 edges,
3887 }
3888 }
3889
3890 #[must_use]
3901 pub fn edges_ordered_indices_borrowed(&self) -> Vec<(usize, usize, usize, &AttrMap)> {
3902 let mut slot_to_pos = vec![0usize; self.slot_names.len()];
3903 for (pos, (_name, &slot)) in self.node_order.iter().enumerate() {
3904 slot_to_pos[slot] = pos;
3905 }
3906 let mut ordered = Vec::with_capacity(self.edge_count);
3907 let mut seen: std::collections::HashSet<(usize, usize, usize)> =
3908 std::collections::HashSet::with_capacity(self.edge_count);
3909 for (u_name, &u_slot) in &self.node_order {
3910 for &v_slot in self.rows[u_slot].keys() {
3911 let pair = (u_slot.min(v_slot), u_slot.max(v_slot));
3912 let Some(bucket) = self.edges.get(&pair) else {
3913 continue;
3914 };
3915 for (key, attrs) in bucket.iter() {
3916 if !seen.insert((pair.0, pair.1, key)) {
3917 continue;
3918 }
3919 let v_name = self.slot_names[v_slot]
3920 .as_deref()
3921 .expect("row references a live slot");
3922 let (left_pos, right_pos) = if u_name.as_str() <= v_name {
3923 (slot_to_pos[u_slot], slot_to_pos[v_slot])
3924 } else {
3925 (slot_to_pos[v_slot], slot_to_pos[u_slot])
3926 };
3927 ordered.push((left_pos, right_pos, key, attrs));
3928 }
3929 }
3930 }
3931 ordered
3932 }
3933
3934 #[cfg(test)]
3941 #[must_use]
3942 pub fn edges_ordered_names(&self) -> Vec<(&str, &str, usize)> {
3943 let mut ordered = Vec::with_capacity(self.edge_count);
3944 let mut seen: std::collections::HashSet<(usize, usize, usize)> =
3945 std::collections::HashSet::with_capacity(self.edge_count);
3946 for (u_name, &u_slot) in &self.node_order {
3947 for &v_slot in self.rows[u_slot].keys() {
3948 let pair = (u_slot.min(v_slot), u_slot.max(v_slot));
3949 let Some(bucket) = self.edges.get(&pair) else {
3950 continue;
3951 };
3952 for key in bucket.key_order() {
3953 if !seen.insert((pair.0, pair.1, key)) {
3954 continue;
3955 }
3956 let v_name = self.slot_names[v_slot]
3957 .as_deref()
3958 .expect("row references a live slot");
3959 let (left, right) = if u_name.as_str() <= v_name {
3960 (u_name.as_str(), v_name)
3961 } else {
3962 (v_name, u_name.as_str())
3963 };
3964 ordered.push((left, right, key));
3965 }
3966 }
3967 }
3968 ordered
3969 }
3970
3971 pub fn clear_edges(&mut self) {
3972 self.edges.clear();
3973 for row in &mut self.rows {
3974 row.clear();
3975 }
3976 self.edge_count = 0;
3977 }
3978
3979 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
3980 where
3981 I: IntoIterator<Item = &'a str>,
3982 {
3983 let remove_slots: rustc_hash::FxHashSet<usize> = nodes
3984 .into_iter()
3985 .filter_map(|name| self.node_order.get(name).copied())
3986 .collect();
3987 if remove_slots.is_empty() {
3988 return (0, 0);
3989 }
3990
3991 let old_edge_count = self.edge_count;
3992 self.edges.retain(|(left, right), _| {
3993 !remove_slots.contains(left) && !remove_slots.contains(right)
3994 });
3995 self.edge_count = self.edges.values().map(CompactBucket::len).sum();
3996
3997 for (slot, row) in self.rows.iter_mut().enumerate() {
3998 if remove_slots.contains(&slot) {
3999 row.clear();
4000 } else {
4001 row.retain(|neighbor, _| !remove_slots.contains(neighbor));
4002 }
4003 }
4004 self.node_order
4005 .retain(|_, slot| !remove_slots.contains(slot));
4006 for &slot in &remove_slots {
4007 self.slot_names[slot] = None;
4008 self.node_attrs[slot].clear();
4009 self.free_slots.push(slot);
4010 }
4011 (
4012 remove_slots.len(),
4013 old_edge_count.saturating_sub(self.edge_count),
4014 )
4015 }
4016
4017 pub fn reorder_rows_for_nx_copy_walk(&mut self) {
4025 let pos = self.slot_positions();
4026 let order_slots: Vec<usize> = self.node_order.values().copied().collect();
4027 let mut new_orders: Vec<(usize, Vec<usize>)> = Vec::with_capacity(order_slots.len());
4028 for &u_slot in &order_slots {
4029 let pu = pos[u_slot];
4030 let mut early: Vec<(usize, usize, usize)> = Vec::new();
4031 let mut late: Vec<usize> = Vec::new();
4032 for &v_slot in self.rows[u_slot].keys() {
4033 let pv = pos[v_slot];
4034 if pv < pu {
4035 let idx = self.rows[v_slot]
4036 .get_index_of(&u_slot)
4037 .unwrap_or(usize::MAX);
4038 early.push((pv, idx, v_slot));
4039 } else {
4040 late.push(v_slot);
4041 }
4042 }
4043 early.sort_unstable();
4044 let mut order: Vec<usize> = early.into_iter().map(|(_, _, v)| v).collect();
4045 order.extend(late);
4046 new_orders.push((u_slot, order));
4047 }
4048 for (u_slot, order) in new_orders {
4049 let mut old = std::mem::take(&mut self.rows[u_slot]);
4050 let mut rebuilt = IndexMap::with_capacity(old.len());
4051 for v_slot in order {
4052 if let Some(cell) = old.shift_remove(&v_slot) {
4053 rebuilt.insert(v_slot, cell);
4054 }
4055 }
4056 debug_assert!(old.is_empty(), "copy-walk reorder dropped a cell");
4057 self.rows[u_slot] = rebuilt;
4058 }
4059 }
4060
4061 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)]) {
4064 for (node, order) in orders {
4065 let Some(&slot) = self.node_order.get(node.as_str()) else {
4066 continue;
4067 };
4068 let mut old = std::mem::take(&mut self.rows[slot]);
4069 let mut new_row = IndexMap::with_capacity(old.len());
4070 for v in order {
4071 if let Some(&v_slot) = self.node_order.get(v.as_str())
4072 && let Some((k, val)) = old.shift_remove_entry(&v_slot)
4073 {
4074 new_row.insert(k, val);
4075 }
4076 }
4077 for (k, val) in old {
4078 new_row.insert(k, val);
4079 }
4080 self.rows[slot] = new_row;
4081 }
4082 }
4083 }
4084}
4085
4086#[derive(Debug, Clone)]
4087pub struct MultiGraph {
4088 mode: CompatibilityMode,
4089 revision: u64,
4090 storage: multigraph_storage::MgSlabStorage,
4091 runtime_policy: RuntimePolicy,
4092 edge_count: usize,
4093 int_adj_cache: IntAdjCache,
4095}
4096
4097pub struct MgNeighborNames<'a> {
4099 rows: indexmap::map::Keys<'a, usize, multigraph_storage::CompactKeys>,
4100 slot_names: &'a [Option<String>],
4101}
4102
4103impl<'a> Iterator for MgNeighborNames<'a> {
4104 type Item = &'a str;
4105
4106 fn next(&mut self) -> Option<Self::Item> {
4107 self.rows.next().map(|&slot| {
4108 self.slot_names[slot]
4109 .as_deref()
4110 .expect("row references a live slot")
4111 })
4112 }
4113}
4114
4115pub struct MgEdgeKeys<'a>(multigraph_storage::CompactBucketKeys<'a>);
4117
4118impl<'a> Iterator for MgEdgeKeys<'a> {
4119 type Item = &'a usize;
4120
4121 fn next(&mut self) -> Option<Self::Item> {
4122 self.0.next()
4123 }
4124}
4125
4126impl MultiGraph {
4127 #[must_use]
4130 pub fn clone_with_fresh_policy(&self) -> Self {
4131 Self {
4132 mode: self.mode,
4133 revision: self.revision,
4134 storage: self.storage.clone(),
4135 runtime_policy: RuntimePolicy::new(self.mode),
4136 edge_count: self.edge_count,
4137 int_adj_cache: IntAdjCache::default(),
4138 }
4139 }
4140
4141 pub fn reorder_rows_for_nx_copy_walk(&mut self) {
4148 self.storage.reorder_rows_for_nx_copy_walk();
4149 *self
4150 .int_adj_cache
4151 .0
4152 .get_mut()
4153 .expect("int_adj_cache poisoned") = None;
4154 }
4155
4156 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)]) {
4160 self.storage.apply_row_orders(orders);
4161 *self
4166 .int_adj_cache
4167 .0
4168 .get_mut()
4169 .expect("int_adj_cache poisoned") = None;
4170 }
4171
4172 #[must_use]
4173 pub fn new(mode: CompatibilityMode) -> Self {
4174 Self {
4175 mode,
4176 revision: 0,
4177 storage: multigraph_storage::MgSlabStorage::new(),
4178 runtime_policy: RuntimePolicy::new(mode),
4179 edge_count: 0,
4180 int_adj_cache: IntAdjCache::default(),
4181 }
4182 }
4183
4184 #[must_use]
4185 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
4186 let mode = runtime_policy.mode();
4187 Self {
4188 mode,
4189 revision: 0,
4190 storage: multigraph_storage::MgSlabStorage::new(),
4191 runtime_policy,
4192 edge_count: 0,
4193 int_adj_cache: IntAdjCache::default(),
4194 }
4195 }
4196
4197 fn slab_store(&self) -> &multigraph_storage::MgSlabStorage {
4198 &self.storage
4199 }
4200
4201 fn slab_store_mut(&mut self) -> &mut multigraph_storage::MgSlabStorage {
4202 &mut self.storage
4203 }
4204
4205 #[must_use]
4206 pub fn strict() -> Self {
4207 Self::new(CompatibilityMode::Strict)
4208 }
4209
4210 #[must_use]
4211 pub fn hardened() -> Self {
4212 Self::new(CompatibilityMode::Hardened)
4213 }
4214
4215 #[must_use]
4216 pub fn mode(&self) -> CompatibilityMode {
4217 self.mode
4218 }
4219
4220 #[must_use]
4221 pub fn node_count(&self) -> usize {
4222 self.slab_store().node_order.len()
4223 }
4224
4225 #[must_use]
4226 pub fn edge_count(&self) -> usize {
4227 self.edge_count
4228 }
4229
4230 #[must_use]
4231 pub fn number_of_selfloops(&self) -> usize {
4232 self.slab_store().number_of_selfloops()
4233 }
4234
4235 #[must_use]
4237 pub fn edge_keys(&self, left: &str, right: &str) -> Option<Vec<usize>> {
4238 self.slab_store().edge_keys(left, right)
4239 }
4240
4241 pub fn edge_keys_iter(&self, left: &str, right: &str) -> Option<MgEdgeKeys<'_>> {
4243 self.slab_store()
4244 .edge_keys_iter(left, right)
4245 .map(MgEdgeKeys)
4246 }
4247
4248 #[must_use]
4249 pub fn revision(&self) -> u64 {
4250 self.revision
4251 }
4252
4253 #[must_use]
4254 pub fn has_node(&self, node: &str) -> bool {
4255 self.slab_store().has_node(node)
4256 }
4257
4258 #[must_use]
4259 pub fn has_edge(&self, left: &str, right: &str) -> bool {
4260 self.slab_store().has_edge(left, right)
4261 }
4262
4263 #[must_use]
4294 pub fn has_edge_by_indices(&self, li: usize, ri: usize) -> bool {
4295 let store = self.slab_store();
4296 match (store.slot_at_position(li), store.slot_at_position(ri)) {
4297 (Some(left), Some(right)) => store.has_edge_by_pair(left, right),
4298 _ => false,
4299 }
4300 }
4301
4302 #[must_use]
4315 pub fn edge_attrs_by_indices(&self, li: usize, ri: usize, key: usize) -> Option<&AttrMap> {
4316 let store = self.slab_store();
4317 match (store.slot_at_position(li), store.slot_at_position(ri)) {
4318 (Some(left), Some(right)) => store.edge_attrs_by_pair(left, right, key),
4319 _ => None,
4320 }
4321 }
4322
4323 #[must_use]
4324 pub fn nodes_ordered(&self) -> Vec<&str> {
4325 self.slab_store()
4326 .node_order
4327 .keys()
4328 .map(String::as_str)
4329 .collect()
4330 }
4331
4332 #[must_use]
4335 #[inline]
4336 pub fn get_node_index(&self, node: &str) -> Option<usize> {
4337 self.slab_store().get_node_index(node)
4338 }
4339
4340 #[must_use]
4342 #[inline]
4343 pub fn get_node_name(&self, index: usize) -> Option<&str> {
4344 self.slab_store().get_node_name(index)
4345 }
4346
4347 #[must_use]
4350 pub fn node_index_matches_int(&self, idx: usize) -> bool {
4351 self.slab_store().node_index_matches_int(idx)
4352 }
4353
4354 #[must_use]
4355 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
4356 self.slab_store().neighbors_by_name(node)
4357 }
4358
4359 pub fn neighbors_iter(&self, node: &str) -> Option<MgNeighborNames<'_>> {
4360 let slab = self.slab_store();
4361 let &slot = slab.node_order.get(node)?;
4362 Some(MgNeighborNames {
4363 rows: slab.rows[slot].keys(),
4364 slot_names: &slab.slot_names,
4365 })
4366 }
4367
4368 fn build_int_adjacency(&self) -> Vec<Vec<usize>> {
4374 self.slab_store().build_int_adjacency()
4375 }
4376
4377 pub fn with_int_adjacency<R>(&self, f: impl FnOnce(&[Vec<usize>]) -> R) -> R {
4385 {
4386 let guard = self.int_adj_cache.0.read().expect("int_adj_cache poisoned");
4387 if let Some((rev, adj)) = guard.as_ref()
4388 && *rev == self.revision
4389 {
4390 return f(adj);
4391 }
4392 }
4393 let adj = self.build_int_adjacency();
4394 let result = f(&adj);
4395 *self
4396 .int_adj_cache
4397 .0
4398 .write()
4399 .expect("int_adj_cache poisoned") = Some((self.revision, adj));
4400 result
4401 }
4402
4403 fn advance_int_adj_memo(&mut self, prev_revision: u64, delta: IntAdjEdgeDelta<'_>) {
4413 let needs_indices = matches!(
4414 delta,
4415 IntAdjEdgeDelta::Add { pair_new: true, .. }
4416 | IntAdjEdgeDelta::Remove {
4417 pair_gone: true,
4418 ..
4419 }
4420 );
4421 let indices = if needs_indices {
4422 let (left, right) = match &delta {
4423 IntAdjEdgeDelta::Add { left, right, .. }
4424 | IntAdjEdgeDelta::Remove { left, right, .. } => (*left, *right),
4425 };
4426 match (
4427 self.storage.get_node_index(left),
4428 self.storage.get_node_index(right),
4429 ) {
4430 (Some(u), Some(v)) => Some((u, v)),
4431 _ => None,
4432 }
4433 } else {
4434 None
4435 };
4436 let Ok(slot) = self.int_adj_cache.0.get_mut() else {
4437 return;
4438 };
4439 if needs_indices && indices.is_none() {
4440 *slot = None;
4443 return;
4444 }
4445 let Some((rev, rows)) = slot.as_mut() else {
4446 return;
4447 };
4448 if *rev != prev_revision {
4449 return;
4450 }
4451 let mut advanced = true;
4452 match delta {
4453 IntAdjEdgeDelta::Add {
4454 left,
4455 right,
4456 left_new,
4457 right_new,
4458 pair_new,
4459 } => {
4460 if left_new {
4461 rows.push(Vec::new());
4462 }
4463 if right_new && left != right {
4464 rows.push(Vec::new());
4465 }
4466 if pair_new && let Some((u, v)) = indices {
4467 rows[u].push(v);
4468 if u != v {
4469 rows[v].push(u);
4470 }
4471 }
4472 }
4473 IntAdjEdgeDelta::Remove { pair_gone, .. } => {
4474 if pair_gone && let Some((u, v)) = indices {
4475 if let Some(pu) = rows[u].iter().position(|&x| x == v) {
4478 rows[u].remove(pu);
4479 if u != v {
4480 if let Some(pv) = rows[v].iter().position(|&x| x == u) {
4481 rows[v].remove(pv);
4482 } else {
4483 advanced = false;
4484 }
4485 }
4486 } else {
4487 advanced = false;
4488 }
4489 }
4490 }
4491 }
4492 if advanced {
4493 *rev = self.revision;
4494 } else {
4495 *slot = None;
4496 }
4497 }
4498
4499 #[must_use]
4500 pub fn edge_keys_vec(&self, left: &str, right: &str) -> Vec<usize> {
4501 self.slab_store().key_order(left, right)
4502 }
4503
4504 #[must_use]
4505 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
4506 self.slab_store().node_attrs_for(node)
4507 }
4508
4509 #[must_use]
4510 pub fn edge_attrs(&self, left: &str, right: &str, key: usize) -> Option<&AttrMap> {
4511 self.slab_store().edge_attrs(left, right, key)
4512 }
4513
4514 pub fn edge_attr_values(
4524 &self,
4525 left: &str,
4526 right: &str,
4527 ) -> Option<impl Iterator<Item = &AttrMap> + '_> {
4528 self.slab_store().edge_attr_values(left, right)
4529 }
4530
4531 #[must_use]
4532 pub fn evidence_ledger(&self) -> &EvidenceLedger {
4533 self.runtime_policy.decision_log()
4534 }
4535
4536 pub fn drain_decision_records(&mut self) -> Vec<DecisionRecord> {
4537 self.runtime_policy.drain_decision_records()
4538 }
4539
4540 #[must_use]
4541 pub fn runtime_policy(&self) -> &RuntimePolicy {
4542 &self.runtime_policy
4543 }
4544
4545 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
4546 self.mode = runtime_policy.mode();
4547 self.runtime_policy = runtime_policy;
4548 }
4549
4550 #[must_use]
4551 pub fn is_directed(&self) -> bool {
4552 false
4553 }
4554
4555 #[must_use]
4556 pub fn is_multigraph(&self) -> bool {
4557 true
4558 }
4559
4560 #[must_use]
4563 pub fn degree(&self, node: &str) -> usize {
4564 self.slab_store().degree(node)
4565 }
4566
4567 #[must_use]
4575 pub fn isolates(&self) -> Vec<String> {
4576 self.slab_store().isolates()
4577 }
4578
4579 #[must_use]
4581 pub fn number_of_isolates(&self) -> usize {
4582 self.slab_store().number_of_isolates()
4583 }
4584
4585 #[must_use]
4588 pub fn is_isolate(&self, node: &str) -> bool {
4589 self.slab_store().is_isolate(node)
4590 }
4591
4592 pub fn add_node(&mut self, node: impl AsRef<str>) -> bool {
4593 self.add_node_with_attrs(node, AttrMap::new())
4594 }
4595
4596 fn add_node_with_attrs_unrecorded(
4604 &mut self,
4605 node: impl AsRef<str>,
4606 attrs: AttrMap,
4607 ) -> (bool, bool, usize) {
4608 let (existed, changed, attrs_count) = self.storage.set_node_attrs(node.as_ref(), attrs);
4609 if changed {
4610 self.revision = self.revision.saturating_add(1);
4611 }
4612 (changed, existed, attrs_count)
4613 }
4614
4615 pub fn add_node_with_attrs(&mut self, node: impl AsRef<str>, attrs: AttrMap) -> bool {
4616 let (changed, existed, attrs_count) = self.add_node_with_attrs_unrecorded(node, attrs);
4617 self.record_decision(
4618 "add_node",
4619 0.0,
4620 false,
4621 vec![
4622 EvidenceTerm {
4623 signal: "node_preexisting".into(),
4624 observed_value: bool_evidence(existed),
4625 log_likelihood_ratio: -3.0,
4626 },
4627 EvidenceTerm {
4628 signal: "attrs_count".into(),
4629 observed_value: count_evidence(attrs_count),
4630 log_likelihood_ratio: -1.0,
4631 },
4632 ],
4633 );
4634 changed
4635 }
4636
4637 pub fn add_edge(
4638 &mut self,
4639 left: impl Into<String>,
4640 right: impl Into<String>,
4641 ) -> Result<usize, GraphError> {
4642 self.add_edge_impl(left, right, None, AttrMap::new())
4643 }
4644
4645 pub fn add_edge_with_attrs(
4646 &mut self,
4647 left: impl Into<String>,
4648 right: impl Into<String>,
4649 attrs: AttrMap,
4650 ) -> Result<usize, GraphError> {
4651 self.add_edge_impl(left, right, None, attrs)
4652 }
4653
4654 pub fn add_edge_with_key_and_attrs(
4655 &mut self,
4656 left: impl Into<String>,
4657 right: impl Into<String>,
4658 key: usize,
4659 attrs: AttrMap,
4660 ) -> Result<usize, GraphError> {
4661 self.add_edge_impl(left, right, Some(key), attrs)
4662 }
4663
4664 #[must_use]
4667 pub fn add_fresh_edge_unrecorded(
4668 &mut self,
4669 left: impl Into<String>,
4670 right: impl Into<String>,
4671 ) -> Option<usize> {
4672 self.add_fresh_edge_with_key_unrecorded(left, right, 0)
4673 }
4674
4675 #[must_use]
4679 pub fn add_fresh_edge_with_key_unrecorded(
4680 &mut self,
4681 left: impl Into<String>,
4682 right: impl Into<String>,
4683 key: usize,
4684 ) -> Option<usize> {
4685 let left = left.into();
4686 let right = right.into();
4687 self.storage.set_node_attrs(&left, AttrMap::new());
4688 if left != right {
4689 self.storage.set_node_attrs(&right, AttrMap::new());
4690 }
4691
4692 if self.storage.has_edge(&left, &right) {
4693 return None;
4694 }
4695 let inserted = self
4696 .storage
4697 .add_keyed_edge(&left, &right, key, AttrMap::new());
4698 debug_assert!(inserted, "fresh edge unexpectedly merged");
4699 self.edge_count += 1;
4700 self.revision = self.revision.saturating_add(1);
4701 Some(key)
4702 }
4703
4704 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
4708 where
4709 I: IntoIterator<Item = (String, AttrMap)>,
4710 {
4711 let mut inserted = 0usize;
4712 for (node, attrs) in nodes {
4713 let (existed, _, _) = self.storage.set_node_attrs(&node, attrs);
4714 if !existed {
4715 inserted += 1;
4716 }
4717 }
4718 if inserted > 0 {
4719 self.revision = self
4720 .revision
4721 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
4722 self.record_decision(
4723 "extend_nodes_with_attrs_unrecorded",
4724 0.0,
4725 false,
4726 vec![EvidenceTerm {
4727 signal: "batch_node_count".into(),
4728 observed_value: count_evidence(inserted),
4729 log_likelihood_ratio: -1.0,
4730 }],
4731 );
4732 }
4733 inserted
4734 }
4735
4736 pub fn extend_keyed_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
4743 where
4744 I: IntoIterator<Item = (String, String, usize, AttrMap)>,
4745 {
4746 let mut inserted = 0usize;
4747 for (left, right, key, attrs) in edges {
4748 if self.storage.add_keyed_edge(&left, &right, key, attrs) {
4749 self.edge_count += 1;
4750 inserted += 1;
4751 }
4752 }
4753 if inserted > 0 {
4754 self.revision = self
4755 .revision
4756 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
4757 self.record_decision(
4758 "extend_keyed_edges_with_attrs_unrecorded",
4759 0.0,
4760 false,
4761 vec![EvidenceTerm {
4762 signal: "batch_edge_count".into(),
4763 observed_value: count_evidence(inserted),
4764 log_likelihood_ratio: -1.0,
4765 }],
4766 );
4767 }
4768 inserted
4769 }
4770
4771 #[must_use]
4778 pub fn extend_fresh_int_prefix_keyed_edges_unrecorded<I>(
4779 &mut self,
4780 node_count: usize,
4781 edges: I,
4782 ) -> usize
4783 where
4784 I: IntoIterator<Item = (usize, usize, usize)>,
4785 {
4786 debug_assert_eq!(self.node_count(), 0);
4787 debug_assert_eq!(self.edge_count(), 0);
4788
4789 let iterator = edges.into_iter();
4790 let (lower_bound, _) = iterator.size_hint();
4791
4792 let node_names = (0..node_count)
4793 .map(|node| node.to_string())
4794 .collect::<Vec<_>>();
4795 let mut fresh_slab = {
4796 use multigraph_storage::MgSlabStorage;
4797 let mut slab = MgSlabStorage::new();
4798 slab.node_order.reserve(node_count);
4799 slab.slot_names.reserve(node_count);
4800 slab.node_attrs.reserve(node_count);
4801 slab.rows.reserve(node_count);
4802 slab.edges.reserve(lower_bound);
4803 for (slot, name) in node_names.iter().enumerate() {
4804 slab.node_order.insert(name.clone(), slot);
4805 slab.slot_names.push(Some(name.clone()));
4806 slab.node_attrs.push(AttrMap::new());
4807 slab.rows.push(IndexMap::new());
4808 }
4809 slab
4810 };
4811
4812 let mut inserted = 0usize;
4813 for (left_idx, right_idx, key) in iterator {
4814 debug_assert!(left_idx < node_names.len());
4815 debug_assert!(right_idx < node_names.len());
4816 {
4817 use multigraph_storage::{CompactBucket, CompactKeys};
4818 let pair = (left_idx.min(right_idx), left_idx.max(right_idx));
4819 match fresh_slab.edges.entry(pair) {
4820 indexmap::map::Entry::Occupied(mut bucket) => {
4821 let _ = bucket.get_mut().merge(key, AttrMap::new());
4822 }
4823 indexmap::map::Entry::Vacant(slot) => {
4824 slot.insert(CompactBucket::One(key, AttrMap::new()));
4825 }
4826 }
4827 match fresh_slab.rows[left_idx].entry(right_idx) {
4828 indexmap::map::Entry::Occupied(mut cell) => {
4829 let _ = cell.get_mut().insert(key);
4830 }
4831 indexmap::map::Entry::Vacant(slot) => {
4832 slot.insert(CompactKeys::One(key));
4833 }
4834 }
4835 if left_idx != right_idx {
4836 match fresh_slab.rows[right_idx].entry(left_idx) {
4837 indexmap::map::Entry::Occupied(mut cell) => {
4838 let _ = cell.get_mut().insert(key);
4839 }
4840 indexmap::map::Entry::Vacant(slot) => {
4841 slot.insert(CompactKeys::One(key));
4842 }
4843 }
4844 }
4845 fresh_slab.edge_count += 1;
4846 }
4847
4848 inserted += 1;
4849 }
4850 self.edge_count = fresh_slab.edge_count;
4851 if inserted > 0 || node_count > 0 {
4852 self.revision = self.revision.saturating_add(
4853 u64::try_from(inserted.saturating_add(node_count)).unwrap_or(u64::MAX),
4854 );
4855 self.record_decision(
4856 "extend_fresh_int_prefix_keyed_edges_unrecorded",
4857 0.0,
4858 false,
4859 vec![EvidenceTerm {
4860 signal: "batch_edge_count".into(),
4861 observed_value: count_evidence(inserted),
4862 log_likelihood_ratio: -1.0,
4863 }],
4864 );
4865 }
4866 debug_assert_eq!(fresh_slab.edge_count, self.edge_count);
4867 self.storage = fresh_slab;
4868 inserted
4869 }
4870
4871 #[must_use]
4877 pub fn extend_fresh_index_keyed_edges_with_attrs_unrecorded<I, N>(
4878 &mut self,
4879 nodes: N,
4880 edges: I,
4881 ) -> usize
4882 where
4883 I: IntoIterator<Item = (usize, usize, usize, AttrMap)>,
4884 N: IntoIterator<Item = String>,
4885 {
4886 if self.node_count() != 0 || self.edge_count() != 0 {
4887 return 0;
4888 }
4889
4890 let node_labels: Vec<String> = nodes.into_iter().collect();
4891 let mut fresh_slab = {
4892 use multigraph_storage::MgSlabStorage;
4893 let mut slab = MgSlabStorage::new();
4894 slab.node_order.reserve(node_labels.len());
4895 slab.slot_names.reserve(node_labels.len());
4896 slab.node_attrs.reserve(node_labels.len());
4897 slab.rows.reserve(node_labels.len());
4898 for (slot, name) in node_labels.iter().enumerate() {
4899 slab.node_order.insert(name.clone(), slot);
4900 slab.slot_names.push(Some(name.clone()));
4901 slab.node_attrs.push(AttrMap::new());
4902 slab.rows.push(IndexMap::new());
4903 }
4904 slab
4905 };
4906
4907 let node_count = node_labels.len();
4908 let mut inserted = 0usize;
4909 for (left_idx, right_idx, key, attrs) in edges {
4910 if node_labels.get(left_idx).is_none() {
4911 continue;
4912 }
4913 if node_labels.get(right_idx).is_none() {
4914 continue;
4915 }
4916 {
4917 use multigraph_storage::{CompactBucket, CompactKeys};
4918 let pair = (left_idx.min(right_idx), left_idx.max(right_idx));
4919 let is_new = match fresh_slab.edges.entry(pair) {
4920 indexmap::map::Entry::Occupied(mut bucket) => {
4921 bucket.get_mut().merge(key, attrs)
4922 }
4923 indexmap::map::Entry::Vacant(slot) => {
4924 slot.insert(CompactBucket::One(key, attrs));
4925 true
4926 }
4927 };
4928 if is_new {
4929 fresh_slab.edge_count += 1;
4930 inserted += 1;
4931 }
4932 match fresh_slab.rows[left_idx].entry(right_idx) {
4933 indexmap::map::Entry::Occupied(mut cell) => {
4934 let _ = cell.get_mut().insert(key);
4935 }
4936 indexmap::map::Entry::Vacant(slot) => {
4937 slot.insert(CompactKeys::One(key));
4938 }
4939 }
4940 if left_idx != right_idx {
4941 match fresh_slab.rows[right_idx].entry(left_idx) {
4942 indexmap::map::Entry::Occupied(mut cell) => {
4943 let _ = cell.get_mut().insert(key);
4944 }
4945 indexmap::map::Entry::Vacant(slot) => {
4946 slot.insert(CompactKeys::One(key));
4947 }
4948 }
4949 }
4950 }
4951 }
4952
4953 self.edge_count = fresh_slab.edge_count;
4954 if node_count > 0 || inserted > 0 {
4955 self.revision = self.revision.saturating_add(
4956 u64::try_from(node_count.saturating_add(inserted)).unwrap_or(u64::MAX),
4957 );
4958 self.record_decision(
4959 "extend_fresh_index_keyed_edges_with_attrs_unrecorded",
4960 0.0,
4961 false,
4962 vec![
4963 EvidenceTerm {
4964 signal: "batch_node_count".into(),
4965 observed_value: count_evidence(node_count),
4966 log_likelihood_ratio: -1.0,
4967 },
4968 EvidenceTerm {
4969 signal: "batch_edge_count".into(),
4970 observed_value: count_evidence(inserted),
4971 log_likelihood_ratio: -1.0,
4972 },
4973 ],
4974 );
4975 }
4976 debug_assert_eq!(fresh_slab.edge_count, self.edge_count);
4977 self.storage = fresh_slab;
4978 inserted
4979 }
4980
4981 fn add_edge_impl(
4982 &mut self,
4983 left: impl Into<String>,
4984 right: impl Into<String>,
4985 explicit_key: Option<usize>,
4986 attrs: AttrMap,
4987 ) -> Result<usize, GraphError> {
4988 let left = left.into();
4989 let right = right.into();
4990 let prev_revision = self.revision;
4993
4994 let unknown_feature = attrs
4995 .keys()
4996 .any(|key| key.starts_with("__fnx_incompatible"));
4997 let incompatibility_probability = if unknown_feature {
4998 1.0
4999 } else if left == right {
5000 0.22
5001 } else {
5002 0.08
5003 };
5004
5005 let action = self
5013 .runtime_policy
5014 .action_for(incompatibility_probability, unknown_feature);
5015
5016 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
5017 self.record_decision(
5018 "add_edge",
5019 incompatibility_probability,
5020 unknown_feature,
5021 vec![EvidenceTerm {
5022 signal: "unknown_incompatible_feature".into(),
5023 observed_value: bool_evidence(unknown_feature),
5024 log_likelihood_ratio: 12.0,
5025 }],
5026 );
5027 return Err(GraphError::FailClosed {
5028 operation: "add_edge",
5029 reason: "incompatible edge metadata".to_owned(),
5030 });
5031 }
5032
5033 let mut left_autocreated = false;
5036 if !self.has_node(&left) {
5037 let _ = self.add_node_with_attrs_unrecorded(left.clone(), AttrMap::new());
5038 left_autocreated = true;
5039 }
5040 let mut right_autocreated = false;
5041 if left == right {
5042 right_autocreated = left_autocreated;
5043 } else if !self.has_node(&right) {
5044 let _ = self.add_node_with_attrs_unrecorded(right.clone(), AttrMap::new());
5045 right_autocreated = true;
5046 }
5047
5048 let left_slot = self
5055 .slab_store()
5056 .node_slot(&left)
5057 .expect("autocreated above");
5058 let right_slot = if left == right {
5059 left_slot
5060 } else {
5061 self.slab_store()
5062 .node_slot(&right)
5063 .expect("autocreated above")
5064 };
5065
5066 let key = explicit_key.unwrap_or_else(|| {
5067 self.slab_store()
5068 .next_edge_key_by_pair(left_slot, right_slot)
5069 });
5070 let pair_was_present = self.slab_store().has_edge_by_pair(left_slot, right_slot);
5071 let changed = self
5072 .slab_store()
5073 .edge_attrs_by_pair(left_slot, right_slot, key)
5074 .is_none_or(|edge_attrs| {
5075 attrs
5076 .iter()
5077 .any(|(attr_key, value)| edge_attrs.get(attr_key) != Some(value))
5078 });
5079 let inserted = self
5080 .slab_store_mut()
5081 .add_keyed_edge_by_pair(left_slot, right_slot, key, attrs);
5082 if inserted {
5083 self.edge_count += 1;
5084 }
5085 let edge_attr_count = self
5086 .slab_store()
5087 .edge_attr_count_by_pair(left_slot, right_slot);
5088 if changed {
5089 self.revision = self.revision.saturating_add(1);
5090 }
5091 if self.revision != prev_revision {
5092 self.advance_int_adj_memo(
5093 prev_revision,
5094 IntAdjEdgeDelta::Add {
5095 left: &left,
5096 right: &right,
5097 left_new: left_autocreated,
5098 right_new: right_autocreated,
5099 pair_new: !pair_was_present,
5100 },
5101 );
5102 }
5103
5104 self.record_decision(
5105 "add_edge",
5106 incompatibility_probability,
5107 unknown_feature,
5108 vec![
5109 EvidenceTerm {
5110 signal: "edge_key".into(),
5111 observed_value: key.to_string().into(),
5112 log_likelihood_ratio: -2.0,
5113 },
5114 EvidenceTerm {
5115 signal: "edge_attr_count".into(),
5116 observed_value: count_evidence(edge_attr_count),
5117 log_likelihood_ratio: -2.0,
5118 },
5119 EvidenceTerm {
5120 signal: "left_autocreated".into(),
5121 observed_value: bool_evidence(left_autocreated),
5122 log_likelihood_ratio: -1.25,
5123 },
5124 EvidenceTerm {
5125 signal: "right_autocreated".into(),
5126 observed_value: bool_evidence(right_autocreated),
5127 log_likelihood_ratio: -1.25,
5128 },
5129 ],
5130 );
5131
5132 Ok(key)
5133 }
5134
5135 pub fn replace_edge_attrs(
5138 &mut self,
5139 left: &str,
5140 right: &str,
5141 key: usize,
5142 attrs: AttrMap,
5143 ) -> bool {
5144 let Some(existing) = self.slab_store().edge_attrs(left, right, key) else {
5145 return false;
5146 };
5147 let changed = *existing != attrs;
5148 if changed {
5149 let replaced = self
5150 .slab_store_mut()
5151 .replace_edge_attrs(left, right, key, attrs);
5152 debug_assert!(replaced, "slab edge disappeared during replacement");
5153 self.revision = self.revision.saturating_add(1);
5154 }
5155 true
5156 }
5157
5158 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
5160 let Some(existing) = self.slab_store().node_attrs_for(node) else {
5161 return false;
5162 };
5163 let changed = *existing != attrs;
5164 if changed {
5165 self.slab_store_mut().replace_node_attrs(node, attrs);
5166 self.revision = self.revision.saturating_add(1);
5167 }
5168 true
5169 }
5170
5171 pub fn remove_edge(&mut self, left: &str, right: &str, key: Option<usize>) -> bool {
5172 let prev_revision = self.revision;
5175 let removal_key = key.or_else(|| {
5176 self.slab_store()
5177 .edge_keys_iter(left, right)
5178 .and_then(|keys| keys.last().copied())
5179 });
5180
5181 let Some(removal_key) = removal_key else {
5182 return false;
5183 };
5184
5185 if !self
5186 .slab_store_mut()
5187 .remove_edge(left, right, Some(removal_key))
5188 {
5189 return false;
5190 }
5191 self.edge_count -= 1;
5192 let pair_gone = !self.slab_store().has_edge(left, right);
5193 self.revision = self.revision.saturating_add(1);
5194 self.advance_int_adj_memo(
5195 prev_revision,
5196 IntAdjEdgeDelta::Remove {
5197 left,
5198 right,
5199 pair_gone,
5200 },
5201 );
5202 true
5203 }
5204
5205 pub fn clear_edges(&mut self) {
5206 if self.edge_count == 0 {
5207 return;
5208 }
5209
5210 self.slab_store_mut().clear_edges();
5211 self.edge_count = 0;
5212 self.revision = self.revision.saturating_add(1);
5213 }
5214
5215 pub fn remove_node(&mut self, node: &str) -> bool {
5216 if !self.has_node(node) {
5217 return false;
5218 }
5219
5220 let removed = self.slab_store_mut().remove_node(node);
5221 debug_assert!(removed, "node disappeared during remove_node");
5222 self.edge_count = self.slab_store().edge_count;
5223 self.revision = self.revision.saturating_add(1);
5224 true
5225 }
5226
5227 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
5235 where
5236 I: IntoIterator<Item = &'a str>,
5237 {
5238 let (removed_nodes, removed_edges) = self.slab_store_mut().remove_nodes_from(nodes);
5239 if removed_nodes == 0 {
5240 return (0, 0);
5241 }
5242 self.edge_count = self.slab_store().edge_count;
5243 self.revision = self
5244 .revision
5245 .saturating_add(u64::try_from(removed_nodes).unwrap_or(u64::MAX));
5246 (removed_nodes, removed_edges)
5247 }
5248
5249 #[must_use]
5250 pub fn edges_ordered(&self) -> Vec<MultiEdgeSnapshot> {
5251 self.slab_store()
5252 .edges_ordered_borrowed()
5253 .into_iter()
5254 .map(|(left, right, key, attrs)| MultiEdgeSnapshot {
5255 left: left.to_owned(),
5256 right: right.to_owned(),
5257 key,
5258 attrs: attrs.clone(),
5259 })
5260 .collect()
5261 }
5262
5263 #[must_use]
5264 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, usize, &AttrMap)> {
5265 self.slab_store().edges_ordered_borrowed()
5266 }
5267
5268 #[must_use]
5275 pub fn weighted_size_int(&self, weight: &str) -> Option<i128> {
5276 self.slab_store().weighted_size_int(weight)
5277 }
5278
5279 #[must_use]
5280 pub fn edges_ordered_indices_borrowed(&self) -> Vec<(usize, usize, usize, &AttrMap)> {
5281 self.slab_store().edges_ordered_indices_borrowed()
5282 }
5283
5284 #[must_use]
5285 pub fn snapshot(&self) -> MultiGraphSnapshot {
5286 self.slab_store().snapshot(self.mode)
5287 }
5288
5289 fn record_decision(
5290 &mut self,
5291 operation: &'static str,
5292 incompatibility_probability: f64,
5293 unknown_incompatible_feature: bool,
5294 evidence: Vec<EvidenceTerm>,
5295 ) -> DecisionAction {
5296 let action = self
5297 .runtime_policy
5298 .action_for(incompatibility_probability, unknown_incompatible_feature);
5299 self.runtime_policy.record(
5300 operation,
5301 action,
5302 incompatibility_probability,
5303 "argmin expected loss over {allow,full_validate,fail_closed}",
5304 evidence,
5305 );
5306 action
5307 }
5308}
5309
5310#[cfg(test)]
5311mod tests {
5312 #[test]
5323 fn multigraph_has_edge_by_indices_matches_the_string_path_across_removals() {
5324 let mut g = MultiGraph::new(CompatibilityMode::Strict);
5325 let names: Vec<String> = (0..8).map(|i| format!("n{i}")).collect();
5326 for name in &names {
5327 g.add_node(name.as_str());
5328 }
5329 for (a, b) in [(0, 1), (2, 3), (3, 5), (5, 7), (0, 7), (4, 4)] {
5330 g.add_edge(names[a].as_str(), names[b].as_str()).unwrap();
5331 }
5332
5333 for step in 0..4 {
5336 if step == 1 {
5337 g.remove_node("n0");
5338 }
5339 if step == 2 {
5340 g.remove_node("n4");
5341 }
5342 if step == 3 {
5343 g.add_node("n8");
5344 g.add_edge("n8", "n3").unwrap();
5345 }
5346 let live: Vec<String> = g.nodes_ordered().iter().map(|s| (*s).to_string()).collect();
5347 for (li, left) in live.iter().enumerate() {
5348 for (ri, right) in live.iter().enumerate() {
5349 let by_index = g.has_edge_by_indices(li, ri);
5350 let by_name = g.has_edge(left.as_str(), right.as_str());
5351 assert_eq!(
5352 by_index, by_name,
5353 "step {step}: has_edge_by_indices({li}, {ri}) disagreed with \
5354 has_edge({left:?}, {right:?})"
5355 );
5356 }
5357 }
5358 let n = live.len();
5360 assert!(!g.has_edge_by_indices(n, 0));
5361 assert!(!g.has_edge_by_indices(0, n + 50));
5362 }
5363 }
5364
5365 #[test]
5388 fn multigraph_node_slot_and_position_are_distinct_index_spaces() {
5389 let mut g = MultiGraph::new(CompatibilityMode::Strict);
5390 for name in ["n0", "n1", "n2", "n3"] {
5391 g.add_node(name);
5392 }
5393 g.add_edge("n2", "n3").unwrap();
5394 assert!(g.has_edge("n2", "n3"));
5395
5396 let pos_before = (g.get_node_index("n2"), g.get_node_index("n3"));
5398 let slot_before = (
5399 g.slab_store().node_slot("n2"),
5400 g.slab_store().node_slot("n3"),
5401 );
5402 assert_eq!(
5403 pos_before, slot_before,
5404 "on a graph with no removals the position and the slot coincide — \
5405 this is why confusing them survives fresh-graph testing"
5406 );
5407
5408 g.remove_node("n0");
5409
5410 let (Some(p2), Some(p3)) = (g.get_node_index("n2"), g.get_node_index("n3")) else {
5411 panic!("nodes vanished after removing an unrelated node");
5412 };
5413 let (Some(s2), Some(s3)) = (
5414 g.slab_store().node_slot("n2"),
5415 g.slab_store().node_slot("n3"),
5416 ) else {
5417 panic!("slots vanished after removing an unrelated node");
5418 };
5419
5420 assert!(
5422 g.has_edge("n2", "n3"),
5423 "removing an unrelated node must not remove this edge"
5424 );
5425
5426 assert_ne!(
5428 (p2, p3),
5429 (s2, s3),
5430 "after a removal the position and slot spaces must diverge; if this \
5431 ever holds, re-derive whether the fast path below is now safe"
5432 );
5433
5434 assert!(
5436 !g.slab_store().has_edge_by_pair(p2, p3),
5437 "position indices happened to answer correctly here — that is luck, \
5438 not a contract; do not take it as licence to feed get_node_index() \
5439 into has_edge_by_pair()"
5440 );
5441
5442 assert!(
5444 g.slab_store().has_edge_by_pair(s2, s3),
5445 "slot indices must agree with the string lookup"
5446 );
5447 }
5448
5449 #[test]
5470 fn multigraph_node_index_matches_int_is_position_keyed_not_slot_keyed() {
5471 let mut g = MultiGraph::new(CompatibilityMode::Strict);
5472 for name in ["0", "1", "2", "3"] {
5473 g.add_node(name);
5474 }
5475
5476 for idx in 0..4 {
5478 assert!(
5479 g.node_index_matches_int(idx),
5480 "on a fresh graph int {idx} sits at position {idx}"
5481 );
5482 assert_eq!(g.get_node_index(&idx.to_string()), Some(idx));
5483 assert_eq!(g.slab_store().node_slot(&idx.to_string()), Some(idx));
5484 }
5485
5486 g.remove_node("0");
5487
5488 let slot_3 = g.slab_store().node_slot("3").expect("node 3 survives");
5490 let pos_3 = g.get_node_index("3").expect("node 3 survives");
5491 assert_eq!(slot_3, 3, "slots are stable across removals");
5492 assert_eq!(pos_3, 2, "positions compact after a removal");
5493 assert_ne!(slot_3, pos_3, "the two spaces have diverged");
5494
5495 assert!(
5499 !g.node_index_matches_int(slot_3),
5500 "slot {slot_3} is past the end of a 3-node order, so a position-keyed predicate reports absent; if this ever answers true the argument has become slot-keyed and every caller must be re-derived"
5501 );
5502
5503 for idx in 0..3 {
5506 assert!(
5507 !g.node_index_matches_int(idx),
5508 "after the removal no name equals its position, so int {idx} must not report as sitting at position {idx}"
5509 );
5510 assert!(
5511 g.has_node(&(idx + 1).to_string()),
5512 "...while the node itself is still very much present"
5513 );
5514 }
5515 }
5516
5517 use super::{AttrMap, Graph, GraphError, MultiGraph};
5518 use fnx_runtime::{CgseValue, CompatibilityMode, DecisionAction, DecisionRecord};
5519 use proptest::prelude::*;
5520 use std::collections::BTreeSet;
5521
5522 #[test]
5527 fn add_edge_newedge_no_duplicate_adjacency() {
5528 let mut g = Graph::strict();
5529 let _ = g.add_edge("h", "a");
5530 let _ = g.add_edge("h", "b");
5531 let _ = g.add_edge("h", "a"); let _ = g.add_edge("a", "h"); let _ = g.add_edge("s", "s"); let _ = g.add_edge("s", "s"); let n = g.node_count();
5536 for i in 0..n {
5537 if let Some(row) = g.neighbors_indices(i) {
5538 let uniq: BTreeSet<usize> = row.iter().copied().collect();
5539 assert_eq!(
5540 uniq.len(),
5541 row.len(),
5542 "node {i} has duplicate adjacency entries"
5543 );
5544 }
5545 }
5546 assert_eq!(
5548 g.edge_count(),
5549 3,
5550 "duplicate/reversed adds must not create new edges"
5551 );
5552 }
5553
5554 #[test]
5562 #[ignore = "measurement; run with --release --ignored --nocapture"]
5563 fn add_edge_newedge_ab() {
5564 use std::hint::black_box;
5565 use std::time::Instant;
5566
5567 let n = 40000usize;
5568 let time = |use_new: bool| -> f64 {
5569 let t0 = Instant::now();
5570 let mut row: Vec<usize> = Vec::with_capacity(n);
5571 for i in 0..n {
5572 if use_new || !row.contains(&i) {
5573 row.push(i);
5574 }
5575 }
5576 black_box(&row);
5577 t0.elapsed().as_secs_f64()
5578 };
5579 for _ in 0..2 {
5580 black_box(time(true));
5581 black_box(time(false));
5582 }
5583 let median = |v: &[f64]| {
5584 let mut s = v.to_vec();
5585 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5586 s[s.len() / 2]
5587 };
5588 let rounds = 41usize;
5589 let paired = |cand: bool, base: bool| -> Vec<f64> {
5590 let mut v = Vec::with_capacity(rounds);
5591 for r in 0..rounds {
5592 let (tb, tc) = if r % 2 == 0 {
5593 let b = time(base);
5594 let c = time(cand);
5595 (b, c)
5596 } else {
5597 let c = time(cand);
5598 let b = time(base);
5599 (b, c)
5600 };
5601 v.push(tb / tc);
5602 }
5603 v
5604 };
5605 let report = |name: &str, ratios: &[f64]| {
5606 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
5607 let mut sorted = ratios.to_vec();
5608 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
5609 println!(
5610 "ADDEDGE_AB {name}: median={:.4}x win_rate={wins}/{rounds} p5_p95=[{:.4},{:.4}]",
5611 median(ratios),
5612 sorted[rounds * 5 / 100],
5613 sorted[rounds * 95 / 100],
5614 );
5615 };
5616 println!("ADDEDGE_AB adj-row push n={n} rounds={rounds} (>1 = new_edge-flag faster)");
5617 report("NEWEDGE_vs_contains", &paired(true, false));
5618 report("NULL_new_vs_new", &paired(true, true));
5619 }
5620
5621 #[test]
5627 #[ignore = "measurement; run with --release --ignored --nocapture"]
5628 fn degree_by_index_selfloop_ab() {
5629 use std::hint::black_box;
5630 use std::time::Instant;
5631
5632 let n = 20000usize;
5633 let deg = 20usize;
5634 let mut g = Graph::strict();
5635 for i in 0..n {
5636 let _ = g.add_node(format!("n{i}"));
5637 }
5638 for i in 0..n {
5639 for step in 1..=deg {
5640 let _ = g.add_edge(format!("n{i}"), format!("n{}", (i + step) % n));
5641 }
5642 }
5643 for i in (0..n).step_by(1000) {
5644 let _ = g.add_edge(format!("n{i}"), format!("n{i}"));
5645 }
5646
5647 for idx in 0..g.node_count() {
5649 let new = g.degree_by_index(idx);
5650 let old = g.adj_indices[idx].len() + usize::from(g.adj_indices[idx].contains(&idx));
5651 assert_eq!(new, old, "degree_by_index parity vs row.contains at {idx}");
5652 assert_eq!(
5653 new,
5654 g.degree(&format!("n{idx}")),
5655 "degree_by_index parity vs &str degree at {idx}"
5656 );
5657 }
5658
5659 let time = |use_new: bool| -> f64 {
5660 let t0 = Instant::now();
5661 let mut sum = 0usize;
5662 for idx in 0..g.node_count() {
5663 sum += if use_new {
5664 g.degree_by_index(idx)
5665 } else {
5666 g.adj_indices[idx].len() + usize::from(g.adj_indices[idx].contains(&idx))
5667 };
5668 }
5669 black_box(sum);
5670 t0.elapsed().as_secs_f64()
5671 };
5672 for _ in 0..3 {
5673 black_box(time(true));
5674 black_box(time(false));
5675 }
5676 let median = |v: &[f64]| {
5677 let mut s = v.to_vec();
5678 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5679 s[s.len() / 2]
5680 };
5681 let rounds = 41usize;
5682 let paired = |cand: bool, base: bool| -> Vec<f64> {
5683 let mut v = Vec::with_capacity(rounds);
5684 for r in 0..rounds {
5685 let (tb, tc) = if r % 2 == 0 {
5686 let b = time(base);
5687 let c = time(cand);
5688 (b, c)
5689 } else {
5690 let c = time(cand);
5691 let b = time(base);
5692 (b, c)
5693 };
5694 v.push(tb / tc);
5695 }
5696 v
5697 };
5698 let report = |name: &str, ratios: &[f64]| {
5699 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
5700 let mut sorted = ratios.to_vec();
5701 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
5702 println!(
5703 "DEGIDX_AB {name}: median={:.4}x win_rate={wins}/{rounds} p5_p95=[{:.4},{:.4}]",
5704 median(ratios),
5705 sorted[rounds * 5 / 100],
5706 sorted[rounds * 95 / 100],
5707 );
5708 };
5709 println!(
5710 "DEGIDX_AB degree histogram n={n} deg={deg} rounds={rounds} (>1 = O(1) has_edge faster)"
5711 );
5712 report("HASEDGE_vs_contains", &paired(true, false));
5713 report("NULL_new_vs_new", &paired(true, true));
5714 }
5715
5716 #[test]
5726 #[ignore = "measurement; run with --release --ignored --nocapture"]
5727 fn iso_edge_count_ab() {
5728 use std::hint::black_box;
5729 use std::time::Instant;
5730
5731 let n = 4000usize;
5732 let deg = 25usize;
5733 let build = |steps: usize| -> Graph {
5734 let mut g = Graph::strict();
5735 for i in 0..n {
5736 let _ = g.add_node(format!("n{i}"));
5737 }
5738 for i in 0..n {
5739 for step in 1..=steps {
5740 let _ = g.add_edge(format!("n{i}"), format!("n{}", (i + step) % n));
5741 }
5742 }
5743 g
5744 };
5745 let g1 = build(deg);
5747 let g2 = build(deg - 1);
5748
5749 assert_eq!(
5751 g1.edge_count(),
5752 g1.edges_ordered().len(),
5753 "edge_count parity g1"
5754 );
5755 assert_eq!(
5756 g2.edge_count(),
5757 g2.edges_ordered().len(),
5758 "edge_count parity g2"
5759 );
5760 assert_ne!(
5761 g1.edge_count(),
5762 g2.edge_count(),
5763 "workload must exercise the differing-count early-out"
5764 );
5765
5766 let time = |use_new: bool| -> f64 {
5767 let t0 = Instant::now();
5768 let mut acc = 0usize;
5769 for _ in 0..8 {
5770 let differ = if use_new {
5771 g1.edge_count() != g2.edge_count()
5772 } else {
5773 g1.edges_ordered().len() != g2.edges_ordered().len()
5774 };
5775 acc += usize::from(differ);
5776 }
5777 black_box(acc);
5778 t0.elapsed().as_secs_f64()
5779 };
5780 for _ in 0..3 {
5781 black_box(time(true));
5782 black_box(time(false));
5783 }
5784 let median = |v: &[f64]| {
5785 let mut s = v.to_vec();
5786 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5787 s[s.len() / 2]
5788 };
5789 let rounds = 41usize;
5790 let paired = |cand: bool, base: bool| -> Vec<f64> {
5791 let mut v = Vec::with_capacity(rounds);
5792 for r in 0..rounds {
5793 let (tb, tc) = if r % 2 == 0 {
5794 let b = time(base);
5795 let c = time(cand);
5796 (b, c)
5797 } else {
5798 let c = time(cand);
5799 let b = time(base);
5800 (b, c)
5801 };
5802 v.push(tb / tc);
5803 }
5804 v
5805 };
5806 let report = |name: &str, ratios: &[f64]| {
5807 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
5808 let mut sorted = ratios.to_vec();
5809 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
5810 println!(
5811 "ISOEDGE_AB {name}: median={:.4}x win_rate={wins}/{rounds} p5_p95=[{:.4},{:.4}]",
5812 median(ratios),
5813 sorted[rounds * 5 / 100],
5814 sorted[rounds * 95 / 100],
5815 );
5816 };
5817 println!(
5818 "ISOEDGE_AB same-node/different-edge early-out n={n} deg={deg} rounds={rounds} (>1 = edge_count faster)"
5819 );
5820 report("EDGECOUNT_vs_edgesordered", &paired(true, false));
5821 report("NULL_new_vs_new", &paired(true, true));
5822 }
5823
5824 #[test]
5832 #[ignore = "measurement; run with --release --ignored --nocapture"]
5833 fn nodes_selfloop_idx_ab() {
5834 use std::hint::black_box;
5835 use std::time::Instant;
5836
5837 let n = 40_000usize;
5839 let deg = 10usize;
5840 let mut g = Graph::strict();
5841 for i in 0..n {
5842 let _ = g.add_node(i.to_string());
5843 }
5844 for i in 0..n {
5845 for step in 1..=deg {
5846 let _ = g.add_edge(i.to_string(), ((i + step) % n).to_string());
5847 }
5848 }
5849
5850 let names = g.nodes_ordered();
5851 let old_scan =
5852 |g: &Graph| -> usize { names.iter().filter(|&&node| g.has_edge(node, node)).count() };
5853 let new_scan = |g: &Graph| -> usize {
5854 (0..names.len())
5855 .filter(|&i| g.has_edge_by_indices(i, i))
5856 .count()
5857 };
5858 assert_eq!(
5859 old_scan(&g),
5860 new_scan(&g),
5861 "nodes_with_selfloops parity (self-loop count)"
5862 );
5863
5864 let time = |cand: bool| -> f64 {
5865 let t0 = Instant::now();
5866 let c = if cand { new_scan(&g) } else { old_scan(&g) };
5867 black_box(c);
5868 t0.elapsed().as_secs_f64()
5869 };
5870 for _ in 0..3 {
5871 black_box(time(true));
5872 black_box(time(false));
5873 }
5874 let median = |v: &[f64]| {
5875 let mut s = v.to_vec();
5876 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5877 s[s.len() / 2]
5878 };
5879 let rounds = 61usize;
5880 let paired = |cand: bool, base: bool| -> Vec<f64> {
5881 let mut v = Vec::with_capacity(rounds);
5882 for r in 0..rounds {
5883 let (tb, tc) = if r % 2 == 0 {
5884 let b = time(base);
5885 let c = time(cand);
5886 (b, c)
5887 } else {
5888 let c = time(cand);
5889 let b = time(base);
5890 (b, c)
5891 };
5892 v.push(tb / tc);
5893 }
5894 v
5895 };
5896 let report = |name: &str, ratios: &[f64]| {
5897 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
5898 let mut sorted = ratios.to_vec();
5899 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
5900 println!(
5901 "SELFLOOPIDX_AB {name}: median={:.4}x win_rate={wins}/{rounds} p5_p95=[{:.4},{:.4}]",
5902 median(ratios),
5903 sorted[rounds * 5 / 100],
5904 sorted[rounds * 95 / 100],
5905 );
5906 };
5907 println!(
5908 "SELFLOOPIDX_AB nodes_with_selfloops n={n} deg={deg} rounds={rounds} (>1 = index probe faster)"
5909 );
5910 report("HASEDGEIDX_vs_hasedge", &paired(true, false));
5911 report("NULL_new_vs_new", &paired(true, true));
5912 }
5913
5914 #[test]
5922 #[ignore = "measurement; run with --release --ignored --nocapture"]
5923 fn number_of_selfloops_idx_ab() {
5924 use std::hint::black_box;
5925 use std::time::Instant;
5926
5927 let n = 20_000usize;
5930 let deg = 20usize;
5931 let mut g = Graph::strict();
5932 for i in 0..n {
5933 let _ = g.add_node(i.to_string());
5934 }
5935 for i in 0..n {
5936 for step in 1..=deg {
5937 let _ = g.add_edge(i.to_string(), ((i + step) % n).to_string());
5938 }
5939 }
5940
5941 let old_count = |g: &Graph| -> usize {
5942 g.edges_ordered()
5943 .iter()
5944 .filter(|e| e.left == e.right)
5945 .count()
5946 };
5947 let new_count = |g: &Graph| -> usize {
5948 (0..g.node_count())
5949 .filter(|&i| g.has_edge_by_indices(i, i))
5950 .count()
5951 };
5952 assert_eq!(old_count(&g), new_count(&g), "number_of_selfloops parity");
5953
5954 let time = |cand: bool| -> f64 {
5955 let t0 = Instant::now();
5956 let c = if cand { new_count(&g) } else { old_count(&g) };
5957 black_box(c);
5958 t0.elapsed().as_secs_f64()
5959 };
5960 for _ in 0..3 {
5961 black_box(time(true));
5962 black_box(time(false));
5963 }
5964 let median = |v: &[f64]| {
5965 let mut s = v.to_vec();
5966 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
5967 s[s.len() / 2]
5968 };
5969 let rounds = 61usize;
5970 let paired = |cand: bool, base: bool| -> Vec<f64> {
5971 let mut v = Vec::with_capacity(rounds);
5972 for r in 0..rounds {
5973 let (tb, tc) = if r % 2 == 0 {
5974 let b = time(base);
5975 let c = time(cand);
5976 (b, c)
5977 } else {
5978 let c = time(cand);
5979 let b = time(base);
5980 (b, c)
5981 };
5982 v.push(tb / tc);
5983 }
5984 v
5985 };
5986 let report = |name: &str, ratios: &[f64]| {
5987 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
5988 let mut sorted = ratios.to_vec();
5989 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
5990 println!(
5991 "NUMSELFIDX_AB {name}: median={:.4}x win_rate={wins}/{rounds} p5_p95=[{:.4},{:.4}]",
5992 median(ratios),
5993 sorted[rounds * 5 / 100],
5994 sorted[rounds * 95 / 100],
5995 );
5996 };
5997 println!(
5998 "NUMSELFIDX_AB number_of_selfloops n={n} deg={deg} rounds={rounds} (>1 = index count faster)"
5999 );
6000 report("INDEXCOUNT_vs_edgesordered", &paired(true, false));
6001 report("NULL_new_vs_new", &paired(true, true));
6002 }
6003
6004 #[test]
6009 fn kneser_push_no_duplicate_adjacency() {
6010 let g = Graph::kneser(CompatibilityMode::Strict, 10, 3);
6012 assert_eq!(g.node_count(), 120, "C(10,3) nodes");
6013 assert_eq!(g.edge_count(), 2100, "120*35/2 Kneser edges");
6014 let mut total_deg = 0usize;
6015 for (i, row) in g.adj_indices.iter().enumerate() {
6016 let uniq: BTreeSet<usize> = row.iter().copied().collect();
6017 assert_eq!(
6018 uniq.len(),
6019 row.len(),
6020 "node {i} has duplicate adjacency entries"
6021 );
6022 assert_eq!(row.len(), 35, "Kneser(10,3) is 35-regular");
6023 total_deg += row.len();
6024 }
6025 assert_eq!(total_deg, 2 * 2100, "handshake: sum of degrees == 2|E|");
6026 }
6027
6028 fn node_name(id: u8) -> String {
6029 format!("n{}", id % 8)
6030 }
6031
6032 fn canonical_edge(left: &str, right: &str) -> (String, String) {
6033 if left <= right {
6034 (left.to_owned(), right.to_owned())
6035 } else {
6036 (right.to_owned(), left.to_owned())
6037 }
6038 }
6039
6040 fn assert_int_adjacency_matches(g: &MultiGraph) {
6047 let expected: Vec<Vec<usize>> = g
6048 .nodes_ordered()
6049 .iter()
6050 .map(|name| {
6051 g.neighbors_iter(name).map_or_else(Vec::new, |it| {
6052 it.map(|v| g.get_node_index(v).unwrap()).collect()
6053 })
6054 })
6055 .collect();
6056 g.with_int_adjacency(|adj| {
6057 assert_eq!(
6058 adj,
6059 expected.as_slice(),
6060 "int adjacency must mirror the stable-slot rows exactly"
6061 );
6062 });
6063 }
6064
6065 #[test]
6066 fn thp6w_int_adjacency_invariant_across_mutations() {
6067 let mut g = MultiGraph::strict();
6068 for i in 0..8 {
6069 g.add_node(format!("n{i}"));
6070 }
6071 let _ = g.add_edge("n0", "n1");
6073 let _ = g.add_edge("n0", "n1"); let _ = g.add_edge("n1", "n2");
6075 let _ = g.add_edge("n2", "n3");
6076 let _ = g.add_edge("n3", "n0");
6077 let _ = g.add_edge("n4", "n4"); let _ = g.add_edge("n5", "n6");
6079 assert_int_adjacency_matches(&g);
6081
6082 assert_int_adjacency_matches(&g);
6085 g.add_node("n9");
6086 assert_int_adjacency_matches(&g);
6087 assert_int_adjacency_matches(&g);
6089 let _ = g.add_edge("n9", "n0");
6090 assert_int_adjacency_matches(&g);
6091 assert_int_adjacency_matches(&g);
6093 let _ = g.add_edge("n9", "n0");
6094 assert_int_adjacency_matches(&g);
6095 assert_int_adjacency_matches(&g);
6097 assert!(g.remove_edge("n0", "n1", None));
6098 assert_int_adjacency_matches(&g);
6099 assert_int_adjacency_matches(&g);
6101 assert!(g.remove_edge("n5", "n6", None));
6102 assert_int_adjacency_matches(&g);
6103 assert_int_adjacency_matches(&g);
6105 assert!(g.remove_node("n2"));
6106 assert_int_adjacency_matches(&g);
6107 assert_int_adjacency_matches(&g);
6110 g.reorder_rows_for_nx_copy_walk();
6111 assert_int_adjacency_matches(&g);
6112 assert_int_adjacency_matches(&g);
6114 g.clear_edges();
6115 assert_int_adjacency_matches(&g);
6116 }
6117
6118 fn int_adj_memo_is_warm(g: &MultiGraph) -> bool {
6122 g.int_adj_cache
6123 .0
6124 .read()
6125 .expect("int_adj_cache poisoned")
6126 .as_ref()
6127 .is_some_and(|(rev, _)| *rev == g.revision)
6128 }
6129
6130 #[test]
6136 fn thp6w_s4_single_edge_mutations_advance_warm_memo() {
6137 let mut g = MultiGraph::strict();
6138 let _ = g.add_edge("a", "b");
6139 assert_int_adjacency_matches(&g); let _ = g.add_edge("b", "c");
6143 assert!(
6144 int_adj_memo_is_warm(&g),
6145 "add_edge must advance, not invalidate"
6146 );
6147 assert_int_adjacency_matches(&g);
6148 let _ = g.add_edge("d", "e");
6150 assert!(int_adj_memo_is_warm(&g));
6151 assert_int_adjacency_matches(&g);
6152 let _ = g.add_edge("f", "f");
6154 assert!(int_adj_memo_is_warm(&g));
6155 assert_int_adjacency_matches(&g);
6156 let _ = g.add_edge("a", "b");
6158 assert!(int_adj_memo_is_warm(&g));
6159 assert_int_adjacency_matches(&g);
6160 let mut attrs = AttrMap::new();
6162 attrs.insert("w".to_owned(), fnx_runtime::CgseValue::Int(2));
6163 let _ = g.add_edge_with_key_and_attrs("a", "b", 0, attrs);
6164 assert!(int_adj_memo_is_warm(&g));
6165 assert_int_adjacency_matches(&g);
6166 assert!(g.remove_edge("a", "b", None));
6168 assert!(int_adj_memo_is_warm(&g));
6169 assert_int_adjacency_matches(&g);
6170 assert!(g.remove_edge("a", "b", None));
6172 assert!(int_adj_memo_is_warm(&g));
6173 assert_int_adjacency_matches(&g);
6174 let _ = g.add_edge("b", "x");
6177 let _ = g.add_edge("b", "y");
6178 assert!(g.remove_edge("b", "c", None));
6179 assert!(int_adj_memo_is_warm(&g));
6180 assert_int_adjacency_matches(&g);
6181 assert!(g.remove_edge("f", "f", None));
6183 assert!(int_adj_memo_is_warm(&g));
6184 assert_int_adjacency_matches(&g);
6185 assert!(g.remove_node("x"));
6187 assert!(
6188 !int_adj_memo_is_warm(&g),
6189 "remove_node stays invalidate-only; a warm memo here would serve stale indices"
6190 );
6191 assert_int_adjacency_matches(&g);
6192 }
6193
6194 #[test]
6203 fn thp6w_s5_int_storage_proto_parity_gates() {
6204 use super::multigraph_storage::MgIntStorageProto;
6205
6206 let stream: Vec<(&str, &str, usize)> = vec![
6207 ("2", "10", 0),
6208 ("10", "2", 1), ("1", "1", 0), ("2", "3", 0),
6211 ("3", "2", 0), ("10", "1", 5), ("0", "2", 0), ("2", "10", 1), ("4", "5", 2),
6216 ("5", "4", 0), ("1", "1", 1), ];
6219 let attrs_for = |i: usize| {
6220 let mut attrs = AttrMap::new();
6221 attrs.insert(
6222 format!("a{}", i % 3),
6223 fnx_runtime::CgseValue::Int(i64::try_from(i).unwrap()),
6224 );
6225 attrs
6226 };
6227
6228 let mut mg = MultiGraph::strict();
6229 let _ = mg.extend_keyed_edges_with_attrs_unrecorded(
6230 stream
6231 .iter()
6232 .enumerate()
6233 .map(|(i, (l, r, k))| ((*l).to_owned(), (*r).to_owned(), *k, attrs_for(i))),
6234 );
6235 let mut proto = MgIntStorageProto::new();
6236 for (i, (l, r, k)) in stream.iter().enumerate() {
6237 proto.add_keyed_edge(l, r, *k, attrs_for(i));
6238 }
6239 let mut compact = super::multigraph_storage::MgIntStorageProtoCompact::new();
6241 for (i, (l, r, k)) in stream.iter().enumerate() {
6242 compact.add_keyed_edge(l, r, *k, attrs_for(i));
6243 }
6244
6245 assert_eq!(
6247 proto
6248 .node_names
6249 .iter()
6250 .map(String::as_str)
6251 .collect::<Vec<_>>(),
6252 mg.nodes_ordered(),
6253 "node order must match"
6254 );
6255 assert_eq!(
6256 compact
6257 .node_names
6258 .iter()
6259 .map(String::as_str)
6260 .collect::<Vec<_>>(),
6261 mg.nodes_ordered(),
6262 "compact node order must match"
6263 );
6264 for (i, name) in mg.nodes_ordered().iter().enumerate() {
6266 let mg_row: Vec<&str> = mg
6267 .neighbors_iter(name)
6268 .map_or_else(Vec::new, Iterator::collect);
6269 assert_eq!(proto.neighbor_names(i), mg_row, "row order for node {name}");
6270 assert_eq!(
6271 compact.neighbor_names(i),
6272 mg_row,
6273 "compact row order for node {name}"
6274 );
6275 for v in &mg_row {
6276 assert_eq!(
6277 proto.key_order(name, v),
6278 mg.edge_keys_vec(name, v),
6279 "key order for pair ({name}, {v})"
6280 );
6281 assert_eq!(
6282 compact.key_order(name, v),
6283 mg.edge_keys_vec(name, v),
6284 "compact key order for pair ({name}, {v})"
6285 );
6286 }
6287 }
6288 assert_eq!(proto.edge_count, mg.edge_count(), "edge_count must match");
6290 assert_eq!(
6291 compact.edge_count,
6292 mg.edge_count(),
6293 "compact edge_count must match"
6294 );
6295 let l = *proto.node_index.get("2").expect("proto must retain node 2");
6298 let r = *proto
6299 .node_index
6300 .get("10")
6301 .expect("proto must retain node 10");
6302 let proto_attrs = proto.edges[&(l.min(r), l.max(r))]
6303 .get(&1)
6304 .expect("proto (2,10,1) cell");
6305 assert_eq!(
6306 Some(proto_attrs),
6307 mg.edge_attrs("2", "10", 1),
6308 "merged attrs on the dup (pair, key) cell must match"
6309 );
6310 assert_eq!(
6311 compact.edges[&(l.min(r), l.max(r))].attrs_for(1),
6312 mg.edge_attrs("2", "10", 1),
6313 "compact merged attrs on the dup (pair, key) cell must match"
6314 );
6315 }
6316
6317 fn assert_proto_matches_mg(
6321 mg: &MultiGraph,
6322 proto: &super::multigraph_storage::MgIntStorageProto,
6323 ) {
6324 assert_eq!(
6325 proto
6326 .node_names
6327 .iter()
6328 .map(String::as_str)
6329 .collect::<Vec<_>>(),
6330 mg.nodes_ordered(),
6331 "node order"
6332 );
6333 for (i, name) in proto.node_names.iter().enumerate() {
6334 assert_eq!(
6335 proto.node_index.get(name),
6336 Some(&i),
6337 "node_index desync for {name}"
6338 );
6339 }
6340 for (i, name) in mg.nodes_ordered().iter().enumerate() {
6341 let mg_row: Vec<&str> = mg
6342 .neighbors_iter(name)
6343 .map_or_else(Vec::new, Iterator::collect);
6344 assert_eq!(proto.neighbor_names(i), mg_row, "row order for {name}");
6345 for v in &mg_row {
6346 assert_eq!(
6347 proto.key_order(name, v),
6348 mg.edge_keys_vec(name, v),
6349 "key order for ({name}, {v})"
6350 );
6351 }
6352 }
6353 assert_eq!(proto.edge_count, mg.edge_count(), "edge_count");
6354 }
6355
6356 #[test]
6362 fn thp6w_s7_removal_renumber_parity_gauntlet() {
6363 use super::multigraph_storage::MgIntStorageProto;
6364
6365 let stream: Vec<(&str, &str, usize)> = vec![
6366 ("2", "10", 0),
6367 ("10", "2", 1),
6368 ("1", "1", 0),
6369 ("2", "3", 0),
6370 ("3", "2", 0),
6371 ("10", "1", 5),
6372 ("0", "2", 0),
6373 ("2", "10", 1),
6374 ("4", "5", 2),
6375 ("5", "4", 0),
6376 ("1", "1", 1),
6377 ];
6378 let mut mg = MultiGraph::strict();
6379 let _ = mg.extend_keyed_edges_with_attrs_unrecorded(
6380 stream
6381 .iter()
6382 .map(|(l, r, k)| ((*l).to_owned(), (*r).to_owned(), *k, AttrMap::new())),
6383 );
6384 let mut proto = MgIntStorageProto::new();
6385 for (l, r, k) in &stream {
6386 proto.add_keyed_edge(l, r, *k, AttrMap::new());
6387 }
6388 assert_proto_matches_mg(&mg, &proto);
6389
6390 assert_eq!(
6393 mg.remove_edge("4", "5", None),
6394 proto.remove_edge("4", "5", None)
6395 );
6396 assert_proto_matches_mg(&mg, &proto);
6397 assert_eq!(
6399 mg.remove_edge("2", "10", Some(0)),
6400 proto.remove_edge("2", "10", Some(0))
6401 );
6402 assert_proto_matches_mg(&mg, &proto);
6403 assert_eq!(
6405 mg.remove_edge("2", "10", None),
6406 proto.remove_edge("2", "10", None)
6407 );
6408 assert_proto_matches_mg(&mg, &proto);
6409 assert_eq!(
6411 mg.remove_edge("1", "1", Some(0)),
6412 proto.remove_edge("1", "1", Some(0))
6413 );
6414 assert_proto_matches_mg(&mg, &proto);
6415 assert_eq!(
6417 mg.remove_edge("0", "10", None),
6418 proto.remove_edge("0", "10", None)
6419 );
6420 assert_eq!(
6421 mg.remove_edge("2", "3", Some(9)),
6422 proto.remove_edge("2", "3", Some(9))
6423 );
6424 assert_proto_matches_mg(&mg, &proto);
6425
6426 assert_eq!(mg.remove_node("2"), proto.remove_node("2"));
6428 assert_proto_matches_mg(&mg, &proto);
6429 mg.extend_keyed_edges_with_attrs_unrecorded(vec![
6431 ("3".to_owned(), "99".to_owned(), 0, AttrMap::new()),
6432 ("0".to_owned(), "3".to_owned(), 0, AttrMap::new()),
6433 ]);
6434 proto.add_keyed_edge("3", "99", 0, AttrMap::new());
6435 proto.add_keyed_edge("0", "3", 0, AttrMap::new());
6436 assert_proto_matches_mg(&mg, &proto);
6437 assert_eq!(mg.remove_node("10"), proto.remove_node("10"));
6439 assert_proto_matches_mg(&mg, &proto);
6440 assert_eq!(mg.remove_node("1"), proto.remove_node("1"));
6441 assert_proto_matches_mg(&mg, &proto);
6442 assert_eq!(mg.remove_node("2"), proto.remove_node("2"));
6444 assert_proto_matches_mg(&mg, &proto);
6445 }
6446
6447 #[test]
6458 #[ignore = "paired A/B benchmark; run --release with --ignored --nocapture"]
6459 fn thp6w_s8_removal_cost_ab() {
6460 use super::multigraph_storage::MgIntStorageProto;
6461 use std::collections::HashMap;
6462 use std::hint::black_box;
6463 use std::time::Instant;
6464
6465 fn push_edge(
6466 stream: &mut Vec<(usize, usize, usize)>,
6467 key_counter: &mut HashMap<(usize, usize), usize>,
6468 u: usize,
6469 v: usize,
6470 ) {
6471 let entry = key_counter.entry((u.min(v), u.max(v))).or_insert(0);
6472 stream.push((u, v, *entry));
6473 *entry += 1;
6474 }
6475
6476 let n = 20000usize;
6477 let mut stream = Vec::new();
6478 let mut key_counter = HashMap::new();
6479 for i in 0..n {
6480 push_edge(&mut stream, &mut key_counter, i, (i + 1) % n);
6481 }
6482 let mut state = 0x2545_F491_4F6C_DD1D_u64;
6483 let draw = |state: &mut u64| -> usize {
6484 *state = state
6485 .wrapping_mul(6_364_136_223_846_793_005)
6486 .wrapping_add(1_442_695_040_888_963_407);
6487 usize::try_from(*state >> 33).unwrap() % n
6488 };
6489 for _ in 0..3 * n {
6490 let u = draw(&mut state);
6491 let v = draw(&mut state);
6492 push_edge(&mut stream, &mut key_counter, u, v);
6493 }
6494 let victims: Vec<String> = (0..n).step_by(100).map(|i| i.to_string()).collect();
6495
6496 let time_current_removals = |stream: &[(usize, usize, usize)]| -> f64 {
6497 let mut g = MultiGraph::strict();
6498 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
6499 let t0 = Instant::now();
6500 for name in &victims {
6501 assert!(g.remove_node(name));
6502 }
6503 let dt = t0.elapsed().as_secs_f64();
6504 black_box(g.edge_count());
6505 dt
6506 };
6507 let time_proto_removals = |stream: &[(usize, usize, usize)]| -> f64 {
6508 let mut proto = MgIntStorageProto::bulk_load_int_prefix(n, stream.iter().copied());
6509 let t0 = Instant::now();
6510 for name in &victims {
6511 assert!(proto.remove_node(name));
6512 }
6513 let dt = t0.elapsed().as_secs_f64();
6514 black_box(proto.edge_count);
6515 dt
6516 };
6517
6518 let mut g = MultiGraph::strict();
6520 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
6521 let mut proto = MgIntStorageProto::bulk_load_int_prefix(n, stream.iter().copied());
6522 for name in &victims {
6523 assert!(g.remove_node(name));
6524 assert!(proto.remove_node(name));
6525 }
6526 assert_eq!(
6527 proto.edge_count,
6528 g.edge_count(),
6529 "removal arms diverged in edge_count"
6530 );
6531 assert_eq!(
6532 proto
6533 .node_names
6534 .iter()
6535 .map(String::as_str)
6536 .collect::<Vec<_>>(),
6537 g.nodes_ordered(),
6538 "removal arms diverged in node order"
6539 );
6540
6541 let median = |samples: &mut Vec<f64>| -> f64 {
6542 samples.sort_by(f64::total_cmp);
6543 samples[samples.len() / 2]
6544 };
6545 let (mut cur, mut pro, mut nul) = (Vec::new(), Vec::new(), Vec::new());
6546 for _ in 0..7 {
6547 cur.push(time_current_removals(&stream));
6548 pro.push(time_proto_removals(&stream));
6549 nul.push(time_current_removals(&stream));
6550 }
6551 let time_slab_removals = |stream: &[(usize, usize, usize)]| -> f64 {
6553 let mut slab = super::multigraph_storage::MgSlabStorage::bulk_load_int_prefix(
6554 n,
6555 stream.iter().copied(),
6556 );
6557 let t0 = Instant::now();
6558 for name in &victims {
6559 assert!(slab.remove_node(name));
6560 }
6561 let dt = t0.elapsed().as_secs_f64();
6562 black_box(slab.edge_count);
6563 dt
6564 };
6565 let mut g2 = MultiGraph::strict();
6566 let _ = g2.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
6567 let mut slab = super::multigraph_storage::MgSlabStorage::bulk_load_int_prefix(
6568 n,
6569 stream.iter().copied(),
6570 );
6571 for name in &victims {
6572 assert!(g2.remove_node(name));
6573 assert!(slab.remove_node(name));
6574 }
6575 assert_eq!(
6576 slab.edge_count,
6577 g2.edge_count(),
6578 "slab removal arm diverged in edge_count"
6579 );
6580 assert_eq!(
6581 slab.node_order
6582 .keys()
6583 .map(String::as_str)
6584 .collect::<Vec<_>>(),
6585 g2.nodes_ordered(),
6586 "slab removal arm diverged in node order"
6587 );
6588 let mut sla = Vec::new();
6589 for _ in 0..7 {
6590 sla.push(time_slab_removals(&stream));
6591 }
6592 let msl = median(&mut sla);
6593
6594 let (mc, mp, mn) = (median(&mut cur), median(&mut pro), median(&mut nul));
6595 println!(
6596 "THP6W_S8_REMOVAL_AB n={n} m={} removals={} current={mc:.6}s proto_renumber={mp:.6}s slab={msl:.6}s ratio_proto_over_current={:.3} ratio_slab_over_current={:.3} null={:.3}",
6597 stream.len(),
6598 victims.len(),
6599 mp / mc,
6600 msl / mc,
6601 mc / mn,
6602 );
6603 }
6604
6605 fn assert_slab_matches_mg(mg: &MultiGraph, slab: &super::multigraph_storage::MgSlabStorage) {
6610 let mg_node_order = mg.nodes_ordered();
6611 assert_eq!(
6612 slab.node_order
6613 .keys()
6614 .map(String::as_str)
6615 .collect::<Vec<_>>(),
6616 mg_node_order,
6617 "slab node order"
6618 );
6619 for (name, &slot) in &slab.node_order {
6620 assert_eq!(
6621 slab.slot_names[slot].as_deref(),
6622 Some(name.as_str()),
6623 "slot_names desync for {name}"
6624 );
6625 }
6626 for &free in &slab.free_slots {
6627 assert!(
6628 slab.slot_names[free].is_none(),
6629 "free slot {free} still named"
6630 );
6631 assert!(slab.rows[free].is_empty(), "free slot {free} has row cells");
6632 assert!(
6633 slab.node_attrs[free].is_empty(),
6634 "free slot {free} still has node attrs"
6635 );
6636 }
6637 for &name in &mg_node_order {
6638 assert_eq!(
6639 slab.node_attrs_for(name),
6640 mg.node_attrs(name),
6641 "slab node attrs for {name}"
6642 );
6643 }
6644 for (pos, &name) in mg_node_order.iter().enumerate() {
6645 let mg_row = mg.neighbors(name).unwrap_or_default();
6646 assert_eq!(
6647 slab.neighbor_names(pos),
6648 mg_row,
6649 "slab row order for {name}"
6650 );
6651 for &v in &mg_row {
6652 let mg_keys = mg.edge_keys_vec(name, v);
6653 assert_eq!(
6654 slab.key_order(name, v),
6655 mg_keys,
6656 "slab key order for ({name}, {v})"
6657 );
6658 }
6659 }
6660 assert_eq!(slab.edge_count, mg.edge_count, "slab edge_count");
6661 }
6662
6663 #[test]
6670 fn thp6w_s9_slab_recycling_parity_gauntlet() {
6671 use super::multigraph_storage::MgSlabStorage;
6672
6673 let stream: Vec<(&str, &str, usize)> = vec![
6674 ("2", "10", 0),
6675 ("10", "2", 1),
6676 ("1", "1", 0),
6677 ("2", "3", 0),
6678 ("3", "2", 0),
6679 ("10", "1", 5),
6680 ("0", "2", 0),
6681 ("2", "10", 1),
6682 ("4", "5", 2),
6683 ("5", "4", 0),
6684 ("1", "1", 1),
6685 ];
6686 let mut mg = MultiGraph::strict();
6687 let _ = mg.extend_keyed_edges_with_attrs_unrecorded(
6688 stream
6689 .iter()
6690 .map(|(l, r, k)| ((*l).to_owned(), (*r).to_owned(), *k, AttrMap::new())),
6691 );
6692 let mut slab = MgSlabStorage::new();
6693 for (l, r, k) in &stream {
6694 slab.add_keyed_edge(l, r, *k, AttrMap::new());
6695 }
6696 assert_slab_matches_mg(&mg, &slab);
6697
6698 assert_eq!(
6700 mg.remove_edge("4", "5", None),
6701 slab.remove_edge("4", "5", None)
6702 );
6703 assert_slab_matches_mg(&mg, &slab);
6704 assert_eq!(
6705 mg.remove_edge("2", "10", Some(0)),
6706 slab.remove_edge("2", "10", Some(0))
6707 );
6708 assert_slab_matches_mg(&mg, &slab);
6709 assert_eq!(
6710 mg.remove_edge("2", "10", None),
6711 slab.remove_edge("2", "10", None)
6712 );
6713 assert_slab_matches_mg(&mg, &slab);
6714 assert_eq!(
6715 mg.remove_edge("1", "1", Some(0)),
6716 slab.remove_edge("1", "1", Some(0))
6717 );
6718 assert_slab_matches_mg(&mg, &slab);
6719 assert_eq!(
6720 mg.remove_edge("0", "10", None),
6721 slab.remove_edge("0", "10", None)
6722 );
6723 assert_eq!(
6724 mg.remove_edge("2", "3", Some(9)),
6725 slab.remove_edge("2", "3", Some(9))
6726 );
6727 assert_slab_matches_mg(&mg, &slab);
6728
6729 assert_eq!(mg.remove_node("2"), slab.remove_node("2"));
6731 assert_slab_matches_mg(&mg, &slab);
6732 mg.extend_keyed_edges_with_attrs_unrecorded(vec![(
6735 "fresh".to_owned(),
6736 "3".to_owned(),
6737 0,
6738 AttrMap::new(),
6739 )]);
6740 slab.add_keyed_edge("fresh", "3", 0, AttrMap::new());
6741 assert_slab_matches_mg(&mg, &slab);
6742 mg.extend_keyed_edges_with_attrs_unrecorded(vec![(
6745 "2".to_owned(),
6746 "fresh".to_owned(),
6747 7,
6748 AttrMap::new(),
6749 )]);
6750 slab.add_keyed_edge("2", "fresh", 7, AttrMap::new());
6751 assert_slab_matches_mg(&mg, &slab);
6752 assert_eq!(mg.remove_node("fresh"), slab.remove_node("fresh"));
6754 assert_slab_matches_mg(&mg, &slab);
6755 assert_eq!(mg.remove_node("1"), slab.remove_node("1"));
6757 assert_slab_matches_mg(&mg, &slab);
6758 assert_eq!(mg.remove_node("10"), slab.remove_node("10"));
6759 assert_slab_matches_mg(&mg, &slab);
6760 assert_eq!(mg.remove_node("gone"), slab.remove_node("gone"));
6762 assert_slab_matches_mg(&mg, &slab);
6763 }
6764
6765 fn assert_slab_edges_ordered_matches_mg(
6769 mg: &MultiGraph,
6770 slab: &super::multigraph_storage::MgSlabStorage,
6771 ) {
6772 let mg_ordered: Vec<(&str, &str, usize)> = mg
6773 .edges_ordered_borrowed()
6774 .into_iter()
6775 .map(|(l, r, k, _)| (l, r, k))
6776 .collect();
6777 assert_eq!(
6778 slab.edges_ordered_names(),
6779 mg_ordered,
6780 "edges_ordered walk emission must match"
6781 );
6782 assert_eq!(
6785 slab.edges_ordered_borrowed(),
6786 mg.edges_ordered_borrowed(),
6787 "attrs-carrying edges_ordered walk must match"
6788 );
6789 }
6790
6791 #[test]
6798 fn thp6w_s10_copy_walk_and_snapshot_orientation_gauntlet() {
6799 use super::multigraph_storage::MgSlabStorage;
6800
6801 let stream: Vec<(&str, &str, usize)> = vec![
6802 ("2", "10", 0),
6803 ("10", "2", 1),
6804 ("1", "1", 0),
6805 ("2", "3", 0),
6806 ("10", "1", 5),
6807 ("0", "2", 0),
6808 ("4", "5", 2),
6809 ("5", "4", 0),
6810 ("3", "0", 0),
6811 ("1", "1", 1),
6812 ("5", "0", 3),
6813 ];
6814 let mut mg = MultiGraph::strict();
6815 let _ = mg.extend_keyed_edges_with_attrs_unrecorded(
6816 stream
6817 .iter()
6818 .map(|(l, r, k)| ((*l).to_owned(), (*r).to_owned(), *k, AttrMap::new())),
6819 );
6820 let mut slab = MgSlabStorage::new();
6821 for (l, r, k) in &stream {
6822 slab.add_keyed_edge(l, r, *k, AttrMap::new());
6823 }
6824 assert_slab_matches_mg(&mg, &slab);
6825 assert_slab_edges_ordered_matches_mg(&mg, &slab);
6826
6827 mg.reorder_rows_for_nx_copy_walk();
6829 slab.reorder_rows_for_nx_copy_walk();
6830 assert_slab_matches_mg(&mg, &slab);
6831 assert_slab_edges_ordered_matches_mg(&mg, &slab);
6832
6833 mg.reorder_rows_for_nx_copy_walk();
6835 slab.reorder_rows_for_nx_copy_walk();
6836 assert_slab_matches_mg(&mg, &slab);
6837 assert_slab_edges_ordered_matches_mg(&mg, &slab);
6838
6839 mg.extend_keyed_edges_with_attrs_unrecorded(vec![
6841 ("0".to_owned(), "9".to_owned(), 0, AttrMap::new()),
6842 ("9".to_owned(), "2".to_owned(), 0, AttrMap::new()),
6843 ]);
6844 slab.add_keyed_edge("0", "9", 0, AttrMap::new());
6845 slab.add_keyed_edge("9", "2", 0, AttrMap::new());
6846 assert_slab_matches_mg(&mg, &slab);
6847 assert_slab_edges_ordered_matches_mg(&mg, &slab);
6848
6849 assert_eq!(mg.remove_node("2"), slab.remove_node("2"));
6851 mg.extend_keyed_edges_with_attrs_unrecorded(vec![(
6852 "fresh".to_owned(),
6853 "9".to_owned(),
6854 0,
6855 AttrMap::new(),
6856 )]);
6857 slab.add_keyed_edge("fresh", "9", 0, AttrMap::new());
6858 assert_slab_matches_mg(&mg, &slab);
6859 assert_slab_edges_ordered_matches_mg(&mg, &slab);
6860 mg.reorder_rows_for_nx_copy_walk();
6861 slab.reorder_rows_for_nx_copy_walk();
6862 assert_slab_matches_mg(&mg, &slab);
6863 assert_slab_edges_ordered_matches_mg(&mg, &slab);
6864 }
6865
6866 #[test]
6873 #[ignore = "paired A/B benchmark; run --release with --ignored --nocapture"]
6874 fn thp6w_s12_edges_ordered_read_ab() {
6875 use std::collections::HashMap;
6876 use std::hint::black_box;
6877 use std::time::Instant;
6878
6879 fn push_edge(
6880 stream: &mut Vec<(usize, usize, usize)>,
6881 key_counter: &mut HashMap<(usize, usize), usize>,
6882 u: usize,
6883 v: usize,
6884 ) {
6885 let entry = key_counter.entry((u.min(v), u.max(v))).or_insert(0);
6886 stream.push((u, v, *entry));
6887 *entry += 1;
6888 }
6889
6890 let n = 20000usize;
6891 let mut stream = Vec::new();
6892 let mut key_counter = HashMap::new();
6893 for i in 0..n {
6894 push_edge(&mut stream, &mut key_counter, i, (i + 1) % n);
6895 }
6896 let mut state = 0x2545_F491_4F6C_DD1D_u64;
6897 let draw = |state: &mut u64| -> usize {
6898 *state = state
6899 .wrapping_mul(6_364_136_223_846_793_005)
6900 .wrapping_add(1_442_695_040_888_963_407);
6901 usize::try_from(*state >> 33).unwrap() % n
6902 };
6903 for _ in 0..3 * n {
6904 let u = draw(&mut state);
6905 let v = draw(&mut state);
6906 push_edge(&mut stream, &mut key_counter, u, v);
6907 }
6908
6909 let mut g = MultiGraph::strict();
6910 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
6911 let slab = g.storage.clone();
6912 assert_eq!(
6913 slab.edges_ordered_borrowed(),
6914 g.edges_ordered_borrowed(),
6915 "arms must emit identical sequences"
6916 );
6917
6918 let time_string = || -> f64 {
6919 let t0 = Instant::now();
6920 let out = g.edges_ordered_borrowed();
6921 let dt = t0.elapsed().as_secs_f64();
6922 black_box(out.len());
6923 dt
6924 };
6925 let time_slab = || -> f64 {
6926 let t0 = Instant::now();
6927 let out = slab.edges_ordered_borrowed();
6928 let dt = t0.elapsed().as_secs_f64();
6929 black_box(out.len());
6930 dt
6931 };
6932 let median = |samples: &mut Vec<f64>| -> f64 {
6933 samples.sort_by(f64::total_cmp);
6934 samples[samples.len() / 2]
6935 };
6936 let (mut cur, mut sla, mut nul) = (Vec::new(), Vec::new(), Vec::new());
6937 for _ in 0..9 {
6938 cur.push(time_string());
6939 sla.push(time_slab());
6940 nul.push(time_string());
6941 }
6942 let (mc, ms, mn) = (median(&mut cur), median(&mut sla), median(&mut nul));
6943 println!(
6944 "THP6W_S12_EDGES_ORDERED_AB n={n} m={} string={mc:.6}s slab={ms:.6}s ratio_string_over_slab={:.3} null={:.3}",
6945 stream.len(),
6946 mc / ms,
6947 mc / mn,
6948 );
6949 }
6950
6951 #[test]
6958 #[ignore = "paired A/B benchmark; run --release with --ignored --nocapture"]
6959 fn thp6w_s13_edges_ordered_indices_ab() {
6960 use std::collections::HashMap;
6961 use std::hint::black_box;
6962 use std::time::Instant;
6963
6964 fn push_edge(
6965 stream: &mut Vec<(usize, usize, usize)>,
6966 key_counter: &mut HashMap<(usize, usize), usize>,
6967 u: usize,
6968 v: usize,
6969 ) {
6970 let entry = key_counter.entry((u.min(v), u.max(v))).or_insert(0);
6971 stream.push((u, v, *entry));
6972 *entry += 1;
6973 }
6974
6975 let n = 20000usize;
6976 let mut stream = Vec::new();
6977 let mut key_counter = HashMap::new();
6978 for i in 0..n {
6979 push_edge(&mut stream, &mut key_counter, i, (i + 1) % n);
6980 }
6981 let mut state = 0x2545_F491_4F6C_DD1D_u64;
6982 let draw = |state: &mut u64| -> usize {
6983 *state = state
6984 .wrapping_mul(6_364_136_223_846_793_005)
6985 .wrapping_add(1_442_695_040_888_963_407);
6986 usize::try_from(*state >> 33).unwrap() % n
6987 };
6988 for _ in 0..3 * n {
6989 let u = draw(&mut state);
6990 let v = draw(&mut state);
6991 push_edge(&mut stream, &mut key_counter, u, v);
6992 }
6993
6994 let mut g = MultiGraph::strict();
6995 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
6996 let slab = g.storage.clone();
6997 assert_eq!(
6998 slab.edges_ordered_indices_borrowed(),
6999 g.edges_ordered_indices_borrowed(),
7000 "arms must emit identical index sequences"
7001 );
7002
7003 let time_string = || -> f64 {
7004 let t0 = Instant::now();
7005 let out = g.edges_ordered_indices_borrowed();
7006 let dt = t0.elapsed().as_secs_f64();
7007 black_box(out.len());
7008 dt
7009 };
7010 let time_slab = || -> f64 {
7011 let t0 = Instant::now();
7012 let out = slab.edges_ordered_indices_borrowed();
7013 let dt = t0.elapsed().as_secs_f64();
7014 black_box(out.len());
7015 dt
7016 };
7017 let median = |samples: &mut Vec<f64>| -> f64 {
7018 samples.sort_by(f64::total_cmp);
7019 samples[samples.len() / 2]
7020 };
7021 let (mut cur, mut sla, mut nul) = (Vec::new(), Vec::new(), Vec::new());
7022 for _ in 0..9 {
7023 cur.push(time_string());
7024 sla.push(time_slab());
7025 nul.push(time_string());
7026 }
7027 let (mc, ms, mn) = (median(&mut cur), median(&mut sla), median(&mut nul));
7028 println!(
7029 "THP6W_S13_EDGES_ORDERED_INDICES_AB n={n} m={} string={mc:.6}s slab={ms:.6}s ratio_string_over_slab={:.3} null={:.3}",
7030 stream.len(),
7031 mc / ms,
7032 mc / mn,
7033 );
7034 }
7035
7036 fn assert_multigraph_storage_invariants(graph: &MultiGraph) {
7037 let storage = &graph.storage;
7038 assert_eq!(storage.slot_names.len(), storage.node_attrs.len());
7039 assert_eq!(storage.slot_names.len(), storage.rows.len());
7040 assert_eq!(graph.edge_count, storage.edge_count);
7041 assert_eq!(
7042 storage.edge_count,
7043 storage
7044 .edges
7045 .values()
7046 .map(super::multigraph_storage::CompactBucket::len)
7047 .sum::<usize>()
7048 );
7049
7050 let mut live_slots = vec![false; storage.slot_names.len()];
7051 for (name, &slot) in &storage.node_order {
7052 assert!(!live_slots[slot], "slot {slot} is assigned twice");
7053 live_slots[slot] = true;
7054 assert_eq!(storage.slot_names[slot].as_deref(), Some(name.as_str()));
7055 }
7056 assert_eq!(
7057 live_slots.iter().filter(|&&live| live).count(),
7058 graph.node_count()
7059 );
7060
7061 let mut free_slots = BTreeSet::new();
7062 for &slot in &storage.free_slots {
7063 assert!(free_slots.insert(slot), "free slot {slot} listed twice");
7064 assert!(!live_slots[slot], "live slot {slot} is also free");
7065 assert!(storage.slot_names[slot].is_none());
7066 assert!(storage.node_attrs[slot].is_empty());
7067 assert!(storage.rows[slot].is_empty());
7068 }
7069
7070 for (left, row) in storage.rows.iter().enumerate() {
7071 if !live_slots[left] {
7072 assert!(row.is_empty(), "tombstoned slot {left} has neighbors");
7073 continue;
7074 }
7075 for (&right, keys) in row {
7076 assert!(live_slots[right], "row points to tombstoned slot {right}");
7077 let pair = (left.min(right), left.max(right));
7078 let bucket = storage
7079 .edges
7080 .get(&pair)
7081 .expect("every row cell must have an edge bucket");
7082 assert_eq!(keys.order(), bucket.key_order());
7083 if left != right {
7084 assert_eq!(
7085 storage.rows[right]
7086 .get(&left)
7087 .expect("undirected reverse row is missing")
7088 .order(),
7089 keys.order()
7090 );
7091 }
7092 }
7093 }
7094 for (&(left, right), bucket) in &storage.edges {
7095 assert!(live_slots[left] && live_slots[right]);
7096 assert_eq!(
7097 storage.rows[left]
7098 .get(&right)
7099 .expect("edge bucket missing left row")
7100 .order(),
7101 bucket.key_order()
7102 );
7103 if left != right {
7104 assert_eq!(
7105 storage.rows[right]
7106 .get(&left)
7107 .expect("edge bucket missing right row")
7108 .order(),
7109 bucket.key_order()
7110 );
7111 }
7112 }
7113 }
7114
7115 #[test]
7116 fn thp6w_s35_slab_is_sole_store_mutation_gauntlet() {
7117 let attr = |key: &str, value: i64| {
7118 let mut attrs = AttrMap::new();
7119 attrs.insert(key.to_owned(), fnx_runtime::CgseValue::Int(value));
7120 attrs
7121 };
7122
7123 let mut graph = MultiGraph::strict();
7124 assert!(graph.add_node_with_attrs("iso", attr("kind", 1)));
7125 assert_eq!(
7126 graph.add_edge_with_key_and_attrs("b", "a", 3, attr("weight", 2)),
7127 Ok(3)
7128 );
7129 assert_eq!(
7130 graph.add_edge_with_key_and_attrs("a", "b", 3, attr("color", 7)),
7131 Ok(3)
7132 );
7133 assert_eq!(
7134 graph.add_edge_with_key_and_attrs("a", "b", 0, AttrMap::new()),
7135 Ok(0)
7136 );
7137 assert_eq!(
7138 graph.add_edge_with_key_and_attrs("c", "c", 5, attr("weight", 11)),
7139 Ok(5)
7140 );
7141 assert_eq!(graph.nodes_ordered(), vec!["iso", "b", "a", "c"]);
7142 assert_eq!(graph.edge_keys_vec("a", "b"), vec![3, 0]);
7143 assert_eq!(
7144 graph.edge_attrs("a", "b", 3),
7145 Some(&{
7146 let mut attrs = attr("weight", 2);
7147 attrs.extend(attr("color", 7));
7148 attrs
7149 })
7150 );
7151 assert_eq!(graph.number_of_selfloops(), 1);
7152 assert_eq!(graph.degree("c"), 2);
7153 assert_eq!(graph.isolates(), vec!["iso"]);
7154 assert_multigraph_storage_invariants(&graph);
7155 assert_int_adjacency_matches(&graph);
7156
7157 let baseline = graph.snapshot();
7158 let cloned = graph.clone_with_fresh_policy();
7159 assert_eq!(cloned.snapshot(), baseline);
7160 assert_multigraph_storage_invariants(&cloned);
7161
7162 let revision = graph.revision();
7163 assert!(graph.replace_edge_attrs("a", "b", 3, attr("replacement", 9)));
7164 assert_eq!(graph.revision(), revision + 1);
7165 let revision = graph.revision();
7166 assert!(graph.replace_edge_attrs("a", "b", 3, attr("replacement", 9)));
7167 assert_eq!(
7168 graph.revision(),
7169 revision,
7170 "no-op edge replace bumped revision"
7171 );
7172 assert!(graph.replace_node_attrs("iso", attr("kind", 4)));
7173 assert!(graph.remove_edge("b", "a", Some(3)));
7174 assert_eq!(graph.edge_keys_vec("a", "b"), vec![0]);
7175 assert!(graph.remove_node("b"));
7176 assert!(!graph.has_edge("a", "b"));
7177 assert_eq!(graph.nodes_ordered(), vec!["iso", "a", "c"]);
7178 assert_eq!(graph.add_edge("d", "a"), Ok(0));
7179 assert_eq!(graph.nodes_ordered(), vec!["iso", "a", "c", "d"]);
7180 assert_multigraph_storage_invariants(&graph);
7181
7182 let (removed_nodes, removed_edges) = graph.remove_nodes_from(["iso", "c", "missing"]);
7183 assert_eq!((removed_nodes, removed_edges), (2, 1));
7184 assert_eq!(graph.nodes_ordered(), vec!["a", "d"]);
7185 assert_eq!(graph.edge_count(), 1);
7186 assert_multigraph_storage_invariants(&graph);
7187 graph.clear_edges();
7188 assert_eq!(graph.edge_count(), 0);
7189 assert_eq!(graph.isolates(), vec!["a", "d"]);
7190 assert_multigraph_storage_invariants(&graph);
7191 }
7192
7193 #[test]
7194 fn thp6w_s35_fresh_loaders_populate_sole_store() {
7195 let mut integer = MultiGraph::strict();
7196 assert_eq!(
7197 integer.extend_fresh_int_prefix_keyed_edges_unrecorded(
7198 4,
7199 [(0, 1, 0), (1, 0, 1), (2, 2, 0), (3, 0, 0)]
7200 ),
7201 4
7202 );
7203 assert_eq!(integer.nodes_ordered(), vec!["0", "1", "2", "3"]);
7204 assert_eq!(integer.edge_keys_vec("0", "1"), vec![0, 1]);
7205 assert_eq!(integer.number_of_selfloops(), 1);
7206 assert_multigraph_storage_invariants(&integer);
7207 assert_int_adjacency_matches(&integer);
7208
7209 let mut weight = AttrMap::new();
7210 weight.insert("weight".to_owned(), fnx_runtime::CgseValue::Int(2));
7211 let mut color = AttrMap::new();
7212 color.insert("color".to_owned(), fnx_runtime::CgseValue::Int(7));
7213 let mut attributed = MultiGraph::strict();
7214 assert_eq!(
7215 attributed.extend_fresh_index_keyed_edges_with_attrs_unrecorded(
7216 ["b", "a", "c"].into_iter().map(str::to_owned),
7217 [
7218 (0, 1, 4, weight),
7219 (1, 0, 4, color),
7220 (2, 2, 0, AttrMap::new()),
7221 (0, 9, 0, AttrMap::new()),
7222 ]
7223 ),
7224 2
7225 );
7226 assert_eq!(attributed.nodes_ordered(), vec!["b", "a", "c"]);
7227 assert_eq!(attributed.edge_count(), 2);
7228 assert_eq!(attributed.edge_keys_vec("a", "b"), vec![4]);
7229 let attrs = attributed
7230 .edge_attrs("a", "b", 4)
7231 .expect("merged attributed edge");
7232 assert_eq!(attrs.len(), 2);
7233 assert!(attrs.contains_key("weight"));
7234 assert!(attrs.contains_key("color"));
7235 assert_multigraph_storage_invariants(&attributed);
7236 }
7237
7238 #[cfg(any())]
7246 #[test]
7247 fn thp6w_s14_fresh_bulk_graph_is_warm_from_birth() {
7248 let mut g = MultiGraph::strict();
7249 let stream = vec![
7250 (0usize, 1usize, 0usize),
7251 (1, 2, 0),
7252 (0, 1, 1), (2, 2, 0), (3, 0, 0),
7255 ];
7256 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(5, stream.into_iter());
7257 assert!(
7258 g.slab_shadow_is_warm(),
7259 "fresh bulk graph must be born warm"
7260 );
7261 {
7262 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7263 assert_slab_matches_mg(&g, &shadow.1);
7264 }
7265 let _ = g.add_edge("2", "4");
7267 assert!(g.slab_shadow_is_warm());
7268 assert!(g.remove_edge("0", "1", None));
7269 assert!(g.slab_shadow_is_warm());
7270 assert!(g.remove_node("1"));
7271 assert!(g.slab_shadow_is_warm());
7272 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7273 assert_slab_matches_mg(&g, &shadow.1);
7274
7275 let mut nattrs = AttrMap::new();
7278 nattrs.insert("color".to_owned(), fnx_runtime::CgseValue::Int(7));
7279 let _ = g.add_node_with_attrs("2", nattrs);
7280 assert!(
7281 g.slab_shadow_is_warm(),
7282 "node-attr set must advance (not stale) the warm shadow"
7283 );
7284 {
7285 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7286 assert_slab_matches_mg(&g, &shadow.1);
7287 assert_eq!(
7288 shadow.1.node_attrs_for("2"),
7289 g.nodes.get("2"),
7290 "advanced shadow must carry the set node attr for node 2"
7291 );
7292 }
7293 let mut nattrs2 = AttrMap::new();
7295 nattrs2.insert("w".to_owned(), fnx_runtime::CgseValue::Int(9));
7296 let _ = g.add_node_with_attrs("attr_only", nattrs2);
7297 assert!(g.slab_shadow_is_warm());
7298 {
7299 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7300 assert_slab_matches_mg(&g, &shadow.1);
7301 assert_eq!(
7302 shadow.1.node_attrs_for("attr_only"),
7303 g.nodes.get("attr_only"),
7304 "advanced shadow must carry a new attr-only node"
7305 );
7306 }
7307 g.sync_slab_shadow();
7309 {
7310 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7311 assert_slab_matches_mg(&g, &shadow.1);
7312 }
7313
7314 let mut h = MultiGraph::strict();
7317 let mut w1 = AttrMap::new();
7318 w1.insert("w".to_owned(), fnx_runtime::CgseValue::Int(1));
7319 let mut w2 = AttrMap::new();
7320 w2.insert("x".to_owned(), fnx_runtime::CgseValue::Int(2));
7321 let _ = h.extend_fresh_index_keyed_edges_with_attrs_unrecorded(
7322 vec!["b".to_owned(), "a".to_owned(), "c".to_owned()],
7323 vec![
7324 (0, 1, 0, w1.clone()),
7325 (1, 0, 0, w2), (2, 2, 0, w1), (0, 9, 0, AttrMap::new()), (1, 2, 3, AttrMap::new()), ],
7330 );
7331 assert!(h.slab_shadow_is_warm(), "attributed fresh graph born warm");
7332 let shadow = h.slab_shadow.as_deref().expect("shadow present");
7333 assert_slab_matches_mg(&h, &shadow.1);
7334 assert_eq!(
7335 shadow.1.edges[&(0, 1)].attrs_for(0),
7336 h.edges
7337 .get(&super::EdgeKeyRef::new("b", "a"))
7338 .and_then(|kb| kb.get(&0)),
7339 "merged attrs on the dup cell must match"
7340 );
7341 }
7342
7343 #[cfg(any())]
7348 #[test]
7349 fn thp6w_s19_remove_nodes_from_advances_shadow() {
7350 let mut g = MultiGraph::strict();
7351 for i in 0..12u32 {
7352 let _ = g.add_edge(&i.to_string(), &((i + 1) % 12).to_string());
7353 }
7354 let _ = g.add_edge("0", "6"); let _ = g.add_edge("3", "3"); g.sync_slab_shadow();
7357 assert!(g.slab_shadow_is_warm());
7358
7359 let (rn, _) = g.remove_nodes_from(["3", "9"].into_iter());
7361 assert_eq!(rn, 2);
7362 assert!(
7363 g.slab_shadow_is_warm(),
7364 "remove_nodes_from fast path must advance the shadow"
7365 );
7366 {
7367 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7368 assert_slab_matches_mg(&g, &shadow.1);
7369 }
7370
7371 let (rn2, _) = g.remove_nodes_from(["0", "1", "2", "4", "5"].into_iter());
7373 assert_eq!(rn2, 5);
7374 assert!(
7375 g.slab_shadow_is_warm(),
7376 "remove_nodes_from fall-through must advance the shadow"
7377 );
7378 {
7379 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7380 assert_slab_matches_mg(&g, &shadow.1);
7381 }
7382
7383 g.add_node("island"); assert!(!g.slab_shadow_is_warm());
7386 let _ = g.remove_nodes_from(["island"].into_iter());
7387 assert!(
7388 !g.slab_shadow_is_warm(),
7389 "remove_nodes_from must not warm a stale shadow"
7390 );
7391 g.sync_slab_shadow();
7392 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7393 assert_slab_matches_mg(&g, &shadow.1);
7394 }
7395
7396 #[cfg(any())]
7400 #[test]
7401 fn thp6w_s20_extend_nodes_with_attrs_advances_shadow() {
7402 let mut g = MultiGraph::strict();
7403 for i in 0..6u32 {
7404 let _ = g.add_edge(&i.to_string(), &((i + 1) % 6).to_string());
7405 }
7406 g.sync_slab_shadow();
7407 assert!(g.slab_shadow_is_warm());
7408
7409 let attr = |k: &str, v: i64| {
7410 let mut a = AttrMap::new();
7411 a.insert(k.to_owned(), fnx_runtime::CgseValue::Int(v));
7412 a
7413 };
7414 let ins = g.extend_nodes_with_attrs_unrecorded(vec![
7416 ("new_a".to_owned(), attr("c", 1)),
7417 ("new_b".to_owned(), attr("c", 2)),
7418 ("0".to_owned(), attr("k", 3)),
7419 ]);
7420 assert_eq!(ins, 2, "two new nodes");
7421 assert!(
7422 g.slab_shadow_is_warm(),
7423 "node-attr batch must advance the shadow"
7424 );
7425 {
7426 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7427 assert_slab_matches_mg(&g, &shadow.1);
7428 assert_eq!(shadow.1.node_attrs_for("new_a"), g.nodes.get("new_a"));
7429 assert_eq!(shadow.1.node_attrs_for("0"), g.nodes.get("0"));
7430 }
7431
7432 let ins2 = g.extend_nodes_with_attrs_unrecorded(vec![("new_a".to_owned(), attr("c", 99))]);
7435 assert_eq!(ins2, 0, "no new nodes");
7436 assert!(
7437 g.slab_shadow_is_warm(),
7438 "attr-only batch keeps the shadow warm"
7439 );
7440 {
7441 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7442 assert_eq!(
7443 shadow.1.node_attrs_for("new_a"),
7444 g.nodes.get("new_a"),
7445 "attr-only update must be reflected in the (still-warm) shadow"
7446 );
7447 }
7448 }
7449
7450 #[cfg(any())]
7454 #[test]
7455 fn thp6w_s21_replace_node_attrs_advances_shadow() {
7456 let attr = |k: &str, v: i64| {
7457 let mut a = AttrMap::new();
7458 a.insert(k.to_owned(), fnx_runtime::CgseValue::Int(v));
7459 a
7460 };
7461 let mut g = MultiGraph::strict();
7462 let _ = g.add_node_with_attrs("x", attr("c", 1));
7463 let _ = g.add_edge("x", "y");
7464 g.sync_slab_shadow();
7465 assert!(g.slab_shadow_is_warm());
7466
7467 assert!(g.replace_node_attrs("x", attr("d", 2)));
7469 assert!(
7470 g.slab_shadow_is_warm(),
7471 "replace_node_attrs must advance the shadow"
7472 );
7473 {
7474 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7475 assert_slab_matches_mg(&g, &shadow.1);
7476 assert_eq!(
7477 shadow.1.node_attrs_for("x"),
7478 g.nodes.get("x"),
7479 "shadow must reflect the overwrite"
7480 );
7481 }
7482
7483 let same = g.nodes.get("x").cloned().unwrap_or_default();
7485 assert!(g.replace_node_attrs("x", same));
7486 assert!(g.slab_shadow_is_warm());
7487
7488 assert!(!g.replace_node_attrs("missing", attr("z", 9)));
7490 assert!(g.slab_shadow_is_warm());
7491 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7492 assert_slab_matches_mg(&g, &shadow.1);
7493 }
7494
7495 #[cfg(any())]
7499 #[test]
7500 fn thp6w_s22_replace_edge_attrs_advances_shadow() {
7501 let attr = |k: &str, v: i64| {
7502 let mut a = AttrMap::new();
7503 a.insert(k.to_owned(), fnx_runtime::CgseValue::Int(v));
7504 a
7505 };
7506 let mut g = MultiGraph::strict();
7507 let _ = g.add_edge_with_key_and_attrs("a", "b", 0, attr("w", 1));
7508 let _ = g.add_edge_with_key_and_attrs("a", "b", 1, attr("w", 2)); let _ = g.add_edge("b", "c");
7510 g.sync_slab_shadow();
7511 assert!(g.slab_shadow_is_warm());
7512
7513 assert!(g.replace_edge_attrs("a", "b", 0, attr("x", 5)));
7515 assert!(
7516 g.slab_shadow_is_warm(),
7517 "replace_edge_attrs must advance the shadow"
7518 );
7519 {
7520 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7521 assert_slab_matches_mg(&g, &shadow.1);
7522 assert_eq!(
7524 shadow.1.edges[&(0, 1)].attrs_for(0),
7525 g.edges
7526 .get(&super::EdgeKeyRef::new("a", "b"))
7527 .and_then(|kb| kb.get(&0)),
7528 "advanced shadow must carry the overwritten cell attrs"
7529 );
7530 assert_eq!(
7531 shadow.1.edges[&(0, 1)].attrs_for(1),
7532 g.edges
7533 .get(&super::EdgeKeyRef::new("a", "b"))
7534 .and_then(|kb| kb.get(&1)),
7535 "the parallel cell (key 1) must be untouched"
7536 );
7537 }
7538
7539 let same = g.edge_attrs("a", "b", 0).cloned().unwrap_or_default();
7541 assert!(g.replace_edge_attrs("a", "b", 0, same));
7542 assert!(g.slab_shadow_is_warm());
7543
7544 assert!(!g.replace_edge_attrs("a", "z", 0, attr("q", 1)));
7546 assert!(g.slab_shadow_is_warm());
7547 let shadow = g.slab_shadow.as_deref().expect("shadow present");
7548 assert_slab_matches_mg(&g, &shadow.1);
7549 }
7550
7551 #[cfg(any())]
7557 #[test]
7558 fn thp6w_s23_node_attrs_reads_route_to_slab() {
7559 let attr = |k: &str, v: i64| {
7560 let mut a = AttrMap::new();
7561 a.insert(k.to_owned(), fnx_runtime::CgseValue::Int(v));
7562 a
7563 };
7564 let mut g = MultiGraph::strict();
7565 let _ = g.add_node_with_attrs("x", attr("c", 1));
7566 let _ = g.add_edge("x", "y"); g.sync_slab_shadow();
7570 assert!(g.slab_shadow_is_warm());
7571 assert_eq!(
7573 g.node_attrs("x"),
7574 g.nodes.get("x"),
7575 "attr node routes to slab"
7576 );
7577 assert_eq!(g.node_attrs("y"), g.nodes.get("y"), "attr-less node routes");
7578 assert_eq!(g.node_attrs("missing"), g.nodes.get("missing"), "both None");
7579 assert!(g.node_attrs("x").is_some_and(|a| a.get("c").is_some()));
7580 assert!(g.replace_node_attrs("x", attr("d", 9)));
7582 assert!(g.slab_shadow_is_warm());
7583 assert_eq!(
7584 g.node_attrs("x"),
7585 g.nodes.get("x"),
7586 "routed read tracks overwrite"
7587 );
7588 let mut h = MultiGraph::strict();
7590 let _ = h.add_node_with_attrs("p", attr("c", 3));
7591 assert!(
7592 !h.slab_shadow_is_warm(),
7593 "fresh individual-add graph has no shadow"
7594 );
7595 assert_eq!(
7596 h.node_attrs("p"),
7597 h.nodes.get("p"),
7598 "unrouted fallback still correct"
7599 );
7600 }
7601
7602 #[cfg(any())]
7607 #[test]
7608 fn thp6w_s24_edge_attrs_reads_route_to_slab() {
7609 let attr = |k: &str, v: i64| {
7610 let mut a = AttrMap::new();
7611 a.insert(k.to_owned(), fnx_runtime::CgseValue::Int(v));
7612 a
7613 };
7614 let gt = |g: &MultiGraph, l: &str, r: &str, k: usize| {
7615 g.edges
7616 .get(&super::EdgeKeyRef::new(l, r))
7617 .and_then(|kb| kb.get(&k))
7618 .cloned()
7619 };
7620 let mut g = MultiGraph::strict();
7621 let _ = g.add_edge_with_key_and_attrs("a", "b", 0, attr("w", 1));
7622 let _ = g.add_edge_with_key_and_attrs("a", "b", 1, attr("w", 2)); let _ = g.add_edge("b", "c");
7624 g.sync_slab_shadow();
7625 assert!(g.slab_shadow_is_warm());
7626 assert_eq!(g.edge_attrs("a", "b", 0).cloned(), gt(&g, "a", "b", 0));
7628 assert_eq!(g.edge_attrs("a", "b", 1).cloned(), gt(&g, "a", "b", 1));
7629 assert_eq!(g.edge_attrs("a", "b", 9), None, "missing key");
7630 assert_eq!(g.edge_attrs("a", "z", 0), None, "missing edge");
7631 assert!(g.replace_edge_attrs("a", "b", 0, attr("x", 8)));
7633 assert!(g.slab_shadow_is_warm());
7634 assert_eq!(g.edge_attrs("a", "b", 0).cloned(), gt(&g, "a", "b", 0));
7635 let mut h = MultiGraph::strict();
7637 let _ = h.add_edge_with_key_and_attrs("p", "q", 0, attr("z", 5));
7638 assert!(!h.slab_shadow_is_warm());
7639 assert_eq!(h.edge_attrs("p", "q", 0).cloned(), gt(&h, "p", "q", 0));
7640 }
7641
7642 #[cfg(any())]
7647 #[test]
7648 fn thp6w_s25_edge_keys_vec_reads_route_to_slab() {
7649 let gt = |g: &MultiGraph, l: &str, r: &str| -> Vec<usize> {
7650 g.edges
7651 .get(&super::EdgeKeyRef::new(l, r))
7652 .map(|b| b.keys().copied().collect())
7653 .unwrap_or_default()
7654 };
7655 let mut g = MultiGraph::strict();
7656 let _ = g.add_edge("a", "b"); let _ = g.add_edge("a", "b"); let _ = g.add_edge("a", "b"); let _ = g.add_edge("b", "c");
7660 g.sync_slab_shadow();
7661 assert!(g.slab_shadow_is_warm());
7662 assert_eq!(
7663 g.edge_keys_vec("a", "b"),
7664 gt(&g, "a", "b"),
7665 "routed key order"
7666 );
7667 assert_eq!(g.edge_keys_vec("b", "c"), gt(&g, "b", "c"));
7668 assert_eq!(
7669 g.edge_keys_vec("a", "z"),
7670 Vec::<usize>::new(),
7671 "missing edge"
7672 );
7673 assert!(g.remove_edge("a", "b", Some(1)));
7675 assert!(g.slab_shadow_is_warm());
7676 assert_eq!(g.edge_keys_vec("a", "b"), gt(&g, "a", "b"), "after removal");
7677 let mut h = MultiGraph::strict();
7679 let _ = h.add_edge("p", "q");
7680 assert!(!h.slab_shadow_is_warm());
7681 assert_eq!(h.edge_keys_vec("p", "q"), gt(&h, "p", "q"));
7682 }
7683
7684 #[cfg(any())]
7689 #[test]
7690 fn thp6w_s26_neighbors_reads_route_to_slab() {
7691 fn gt<'a>(g: &'a MultiGraph, n: &str) -> Option<Vec<&'a str>> {
7692 g.adjacency
7693 .get(n)
7694 .map(|row| row.keys().map(String::as_str).collect())
7695 }
7696 let mut g = MultiGraph::strict();
7697 let _ = g.add_edge("a", "b");
7698 let _ = g.add_edge("a", "c");
7699 let _ = g.add_edge("a", "b"); let _ = g.add_edge("c", "d");
7701 g.sync_slab_shadow();
7702 assert!(g.slab_shadow_is_warm());
7703 assert_eq!(
7704 g.neighbors("a"),
7705 gt(&g, "a"),
7706 "routed distinct neighbors, row order"
7707 );
7708 assert_eq!(g.neighbors("c"), gt(&g, "c"));
7709 assert_eq!(g.neighbors("missing"), None, "absent node -> None");
7710 assert_eq!(g.neighbors("missing"), gt(&g, "missing"));
7711 assert!(g.remove_edge("a", "c", None));
7713 assert!(g.slab_shadow_is_warm());
7714 assert_eq!(g.neighbors("a"), gt(&g, "a"), "after removal");
7715 let mut h = MultiGraph::strict();
7717 let _ = h.add_edge("p", "q");
7718 assert!(!h.slab_shadow_is_warm());
7719 assert_eq!(h.neighbors("p"), gt(&h, "p"));
7720 }
7721
7722 #[cfg(any())]
7728 #[test]
7729 fn thp6w_s27_degree_reads_route_to_slab() {
7730 let gt = |g: &MultiGraph, n: &str| -> usize {
7731 g.adjacency.get(n).map_or(0, |row| {
7732 let mut d = 0;
7733 for (nbr, keys) in row {
7734 let c = keys.len();
7735 if nbr.as_str() == n {
7736 d += c * 2;
7737 } else {
7738 d += c;
7739 }
7740 }
7741 d
7742 })
7743 };
7744 let mut g = MultiGraph::strict();
7745 let _ = g.add_edge("a", "b");
7746 let _ = g.add_edge("a", "b"); let _ = g.add_edge("a", "c");
7748 let _ = g.add_edge("a", "a"); g.sync_slab_shadow();
7750 assert!(g.slab_shadow_is_warm());
7751 assert_eq!(g.degree("a"), gt(&g, "a"), "self-loop double + parallel");
7752 assert_eq!(g.degree("b"), gt(&g, "b"));
7753 assert_eq!(g.degree("missing"), 0, "absent node -> 0");
7754 assert_eq!(g.degree("missing"), gt(&g, "missing"));
7755 assert!(g.remove_edge("a", "b", None));
7757 assert!(g.slab_shadow_is_warm());
7758 assert_eq!(g.degree("a"), gt(&g, "a"), "after removal");
7759 let mut h = MultiGraph::strict();
7761 let _ = h.add_edge("p", "q");
7762 assert!(!h.slab_shadow_is_warm());
7763 assert_eq!(h.degree("p"), gt(&h, "p"));
7764 }
7765
7766 #[cfg(any())]
7771 #[test]
7772 fn thp6w_s28_membership_reads_route_to_slab() {
7773 let he = |g: &MultiGraph, l: &str, r: &str| {
7774 g.edges
7775 .get(&super::EdgeKeyRef::new(l, r))
7776 .is_some_and(|b| !b.is_empty())
7777 };
7778 let mut g = MultiGraph::strict();
7779 let _ = g.add_edge("a", "b");
7780 let _ = g.add_edge("b", "c");
7781 let _ = g.add_node("iso"); g.sync_slab_shadow();
7783 assert!(g.slab_shadow_is_warm());
7784 assert_eq!(g.has_node("a"), g.nodes.contains_key("a"));
7786 assert_eq!(g.has_node("iso"), g.nodes.contains_key("iso"));
7787 assert_eq!(g.has_node("missing"), g.nodes.contains_key("missing"));
7788 assert!(g.has_node("iso") && !g.has_node("missing"));
7789 assert_eq!(g.has_edge("a", "b"), he(&g, "a", "b"));
7791 assert_eq!(g.has_edge("b", "a"), he(&g, "b", "a"));
7792 assert_eq!(g.has_edge("a", "c"), he(&g, "a", "c"));
7793 assert_eq!(g.has_edge("x", "y"), he(&g, "x", "y"));
7794 assert!(g.has_edge("a", "b") && g.has_edge("b", "a") && !g.has_edge("a", "c"));
7795 assert!(g.remove_edge("a", "b", None));
7797 assert!(g.slab_shadow_is_warm());
7798 assert_eq!(g.has_edge("a", "b"), he(&g, "a", "b"));
7799 assert!(!g.has_edge("a", "b"));
7800 let mut h = MultiGraph::strict();
7802 let _ = h.add_edge("p", "q");
7803 assert!(!h.slab_shadow_is_warm());
7804 assert_eq!(h.has_edge("p", "q"), he(&h, "p", "q"));
7805 assert_eq!(h.has_node("p"), h.nodes.contains_key("p"));
7806 }
7807
7808 #[cfg(any())]
7813 #[test]
7814 fn thp6w_s29_nodes_ordered_reads_route_to_slab() {
7815 fn gt<'a>(g: &'a MultiGraph) -> Vec<&'a str> {
7816 g.nodes.keys().map(String::as_str).collect()
7817 }
7818 let mut g = MultiGraph::strict();
7819 let _ = g.add_edge("c", "a"); let _ = g.add_edge("a", "b"); let _ = g.add_node("z"); g.sync_slab_shadow();
7823 assert!(g.slab_shadow_is_warm());
7824 assert_eq!(g.nodes_ordered(), gt(&g), "routed node order == String");
7825 assert_eq!(
7826 g.nodes_ordered(),
7827 vec!["c", "a", "b", "z"],
7828 "insertion order"
7829 );
7830 assert!(g.remove_node("a"));
7832 assert!(g.slab_shadow_is_warm());
7833 assert_eq!(g.nodes_ordered(), gt(&g), "after removal");
7834 assert_eq!(g.nodes_ordered(), vec!["c", "b", "z"]);
7835 let mut h = MultiGraph::strict();
7837 let _ = h.add_edge("p", "q");
7838 assert!(!h.slab_shadow_is_warm());
7839 assert_eq!(h.nodes_ordered(), gt(&h));
7840 }
7841
7842 #[cfg(any())]
7848 #[test]
7849 fn thp6w_s30_is_isolate_reads_route_to_slab() {
7850 let gt = |g: &MultiGraph, n: &str| -> bool {
7851 g.nodes.contains_key(n) && g.adjacency.get(n).is_none_or(|row| row.is_empty())
7852 };
7853 let mut g = MultiGraph::strict();
7854 let _ = g.add_edge("a", "b");
7855 let _ = g.add_node("iso"); let _ = g.add_edge("s", "s"); g.sync_slab_shadow();
7858 assert!(g.slab_shadow_is_warm());
7859 assert_eq!(g.is_isolate("iso"), gt(&g, "iso"));
7860 assert!(g.is_isolate("iso"));
7861 assert_eq!(g.is_isolate("a"), gt(&g, "a"));
7862 assert!(!g.is_isolate("a"));
7863 assert_eq!(g.is_isolate("s"), gt(&g, "s"));
7864 assert!(!g.is_isolate("s"), "self-loop node is not isolated");
7865 assert_eq!(g.is_isolate("missing"), gt(&g, "missing"));
7866 assert!(!g.is_isolate("missing"));
7867 assert!(g.remove_edge("a", "b", None));
7869 assert!(g.slab_shadow_is_warm());
7870 assert_eq!(g.is_isolate("a"), gt(&g, "a"));
7871 assert!(g.is_isolate("a"));
7872 let mut h = MultiGraph::strict();
7874 let _ = h.add_node("lonely");
7875 assert!(!h.slab_shadow_is_warm());
7876 assert_eq!(h.is_isolate("lonely"), gt(&h, "lonely"));
7877 }
7878
7879 #[cfg(any())]
7885 #[test]
7886 fn thp6w_s31_weighted_size_int_routes_to_slab() {
7887 fn gt(g: &MultiGraph, w: &str) -> Option<i128> {
7888 let mut total = 0i128;
7889 for bucket in g.edges.values() {
7890 for attrs in bucket.values() {
7891 let v = match attrs.get(w) {
7892 Some(fnx_runtime::CgseValue::Int(x)) => i128::from(*x),
7893 Some(_) => return None,
7894 None => 1,
7895 };
7896 total = total.checked_add(v)?;
7897 }
7898 }
7899 Some(total)
7900 }
7901 let w = |v: i64| -> AttrMap {
7902 let mut a = AttrMap::new();
7903 a.insert("weight".to_owned(), fnx_runtime::CgseValue::Int(v));
7904 a
7905 };
7906 let mut g = MultiGraph::strict();
7907 let _ = g.add_edge_with_key_and_attrs("a", "b", 0, w(5));
7908 let _ = g.add_edge_with_key_and_attrs("a", "b", 1, w(3)); let _ = g.add_edge("b", "c"); let _ = g.add_edge_with_key_and_attrs("c", "c", 0, w(2)); g.sync_slab_shadow();
7912 assert!(g.slab_shadow_is_warm());
7913 assert_eq!(
7914 g.weighted_size_int("weight"),
7915 gt(&g, "weight"),
7916 "routed == String"
7917 );
7918 assert_eq!(g.weighted_size_int("weight"), Some(11), "5+3+1+2");
7919 assert!(g.remove_edge("a", "b", Some(0)));
7921 assert!(g.slab_shadow_is_warm());
7922 assert_eq!(g.weighted_size_int("weight"), gt(&g, "weight"));
7923 assert_eq!(g.weighted_size_int("weight"), Some(6), "3+1+2");
7924 let mut fattr = AttrMap::new();
7926 fattr.insert("weight".to_owned(), fnx_runtime::CgseValue::Float(1.5));
7927 let _ = g.add_edge_with_key_and_attrs("d", "e", 0, fattr);
7928 assert!(g.slab_shadow_is_warm());
7929 assert_eq!(g.weighted_size_int("weight"), gt(&g, "weight"));
7930 assert!(
7931 g.weighted_size_int("weight").is_none(),
7932 "non-int weight -> None"
7933 );
7934 let mut h = MultiGraph::strict();
7936 let _ = h.add_edge_with_key_and_attrs("p", "q", 0, w(7));
7937 assert!(!h.slab_shadow_is_warm());
7938 assert_eq!(h.weighted_size_int("weight"), gt(&h, "weight"));
7939 assert_eq!(h.weighted_size_int("weight"), Some(7));
7940 }
7941
7942 #[cfg(any())]
7948 #[test]
7949 fn thp6w_s32_neighbors_iter_reads_route_to_slab() {
7950 fn gt<'a>(g: &'a MultiGraph, n: &str) -> Option<Vec<&'a str>> {
7951 g.adjacency
7952 .get(n)
7953 .map(|row| row.keys().map(String::as_str).collect())
7954 }
7955 let collect = |g: &MultiGraph, n: &str| -> Option<Vec<String>> {
7956 g.neighbors_iter(n)
7957 .map(|it| it.map(str::to_owned).collect())
7958 };
7959 let want = |g: &MultiGraph, n: &str| -> Option<Vec<String>> {
7960 gt(g, n).map(|v| v.into_iter().map(str::to_owned).collect())
7961 };
7962 let mut g = MultiGraph::strict();
7963 let _ = g.add_edge("a", "b");
7964 let _ = g.add_edge("a", "c");
7965 let _ = g.add_edge("a", "b"); let _ = g.add_edge("c", "d");
7967 g.sync_slab_shadow();
7968 assert!(g.slab_shadow_is_warm());
7969 assert_eq!(
7970 collect(&g, "a"),
7971 want(&g, "a"),
7972 "routed iter == String, row order"
7973 );
7974 assert_eq!(collect(&g, "c"), want(&g, "c"));
7975 assert!(g.neighbors_iter("missing").is_none(), "absent node -> None");
7976 assert!(g.remove_edge("a", "c", None));
7978 assert!(g.slab_shadow_is_warm());
7979 assert_eq!(collect(&g, "a"), want(&g, "a"), "after removal");
7980 let mut h = MultiGraph::strict();
7982 let _ = h.add_edge("p", "q");
7983 assert!(!h.slab_shadow_is_warm());
7984 assert_eq!(collect(&h, "p"), want(&h, "p"));
7985 }
7986
7987 #[cfg(any())]
7994 #[test]
7995 fn thp6w_s33_edge_keys_iter_reads_route_to_slab() {
7996 fn gt(g: &MultiGraph, l: &str, r: &str) -> Option<Vec<usize>> {
7997 g.adjacency
7998 .get(l)?
7999 .get(r)
8000 .map(|keys| keys.iter().copied().collect())
8001 }
8002 let collect = |g: &MultiGraph, l: &str, r: &str| -> Option<Vec<usize>> {
8003 g.edge_keys_iter(l, r).map(|it| it.copied().collect())
8004 };
8005 let mut g = MultiGraph::strict();
8006 let _ = g.add_edge("a", "b"); let _ = g.add_edge("a", "b"); let _ = g.add_edge("a", "b"); let _ = g.add_edge("b", "c"); g.sync_slab_shadow();
8011 assert!(g.slab_shadow_is_warm());
8012 assert_eq!(collect(&g, "a", "b"), gt(&g, "a", "b"), "Many == String");
8013 assert_eq!(collect(&g, "a", "b"), Some(vec![0, 1, 2]));
8014 assert_eq!(collect(&g, "b", "c"), gt(&g, "b", "c"), "One == String");
8015 assert_eq!(collect(&g, "b", "c"), Some(vec![0]));
8016 assert!(g.edge_keys_iter("a", "z").is_none(), "missing edge -> None");
8017 assert!(g.remove_edge("a", "b", Some(1)));
8019 assert!(g.slab_shadow_is_warm());
8020 assert_eq!(collect(&g, "a", "b"), gt(&g, "a", "b"), "after removal");
8021 assert_eq!(collect(&g, "a", "b"), Some(vec![0, 2]));
8022 let mut h = MultiGraph::strict();
8024 let _ = h.add_edge("p", "q");
8025 assert!(!h.slab_shadow_is_warm());
8026 assert_eq!(collect(&h, "p", "q"), gt(&h, "p", "q"));
8027 }
8028
8029 #[cfg(any())]
8037 #[test]
8038 fn thp6w_s11_slab_shadow_dual_write_gauntlet() {
8039 let shadow_parity = |g: &MultiGraph| {
8040 let shadow = g.slab_shadow.as_deref().expect("shadow must be present");
8041 assert_eq!(shadow.0, g.revision, "shadow must be warm (advanced)");
8042 assert_slab_matches_mg(g, &shadow.1);
8043 };
8044
8045 let mut g = MultiGraph::strict();
8046 let _ = g.add_edge("a", "b");
8047 g.sync_slab_shadow();
8048 assert!(g.slab_shadow_is_warm());
8049 shadow_parity(&g);
8050
8051 let _ = g.add_edge("b", "c"); shadow_parity(&g);
8054 let _ = g.add_edge("a", "b"); shadow_parity(&g);
8056 let _ = g.add_edge("d", "d"); shadow_parity(&g);
8058 let mut attrs = AttrMap::new();
8059 attrs.insert("w".to_owned(), fnx_runtime::CgseValue::Int(3));
8060 let _ = g.add_edge_with_key_and_attrs("a", "b", 0, attrs); shadow_parity(&g);
8062
8063 let _ = g.extend_keyed_edges_with_attrs_unrecorded(vec![
8065 ("x".to_owned(), "y".to_owned(), 0, AttrMap::new()),
8066 ("y".to_owned(), "x".to_owned(), 1, AttrMap::new()),
8067 ("a".to_owned(), "x".to_owned(), 0, AttrMap::new()),
8068 ]);
8069 shadow_parity(&g);
8070
8071 assert!(g.remove_edge("a", "b", None));
8074 shadow_parity(&g);
8075 assert!(g.remove_edge("a", "b", None));
8076 shadow_parity(&g);
8077 assert!(g.remove_node("b"));
8078 shadow_parity(&g);
8079 let _ = g.add_edge("recycled", "c");
8080 shadow_parity(&g);
8081
8082 g.reorder_rows_for_nx_copy_walk();
8086 assert!(
8087 g.slab_shadow_is_warm(),
8088 "apply_row_orders must advance (not drop) the warm shadow"
8089 );
8090 shadow_parity(&g);
8091 g.sync_slab_shadow();
8093 shadow_parity(&g);
8094
8095 g.add_node("island");
8097 assert!(
8098 !g.slab_shadow_is_warm(),
8099 "uninstrumented mutation must stale the shadow"
8100 );
8101 g.sync_slab_shadow();
8102 shadow_parity(&g);
8103 g.clear_edges();
8104 assert!(!g.slab_shadow_is_warm());
8105 g.sync_slab_shadow();
8106 shadow_parity(&g);
8107
8108 let _ = g.add_edge("r1", "r2"); g.add_node("stale_maker"); assert!(!g.slab_shadow_is_warm());
8115 let _ = g.add_edge("r2", "r1"); let _ = g.add_edge("r3", "r3");
8117 let expected: Vec<(String, String, usize, AttrMap)> = g
8118 .edges_ordered_borrowed() .into_iter()
8120 .map(|(l, r, k, a)| (l.to_owned(), r.to_owned(), k, a.clone()))
8121 .collect();
8122 let expected_owned: Vec<(String, String, usize, AttrMap)> = g
8124 .edges_ordered() .into_iter()
8126 .map(|e| (e.left, e.right, e.key, e.attrs))
8127 .collect();
8128 g.sync_slab_shadow();
8129 assert!(g.slab_shadow_is_warm());
8130 let served: Vec<(String, String, usize, AttrMap)> = g
8131 .edges_ordered_borrowed() .into_iter()
8133 .map(|(l, r, k, a)| (l.to_owned(), r.to_owned(), k, a.clone()))
8134 .collect();
8135 assert_eq!(
8136 served, expected,
8137 "shadow-served edges_ordered must equal the String walk"
8138 );
8139 let served_owned: Vec<(String, String, usize, AttrMap)> = g
8140 .edges_ordered() .into_iter()
8142 .map(|e| (e.left, e.right, e.key, e.attrs))
8143 .collect();
8144 assert_eq!(
8145 served_owned, expected_owned,
8146 "shadow-served owned edges_ordered must equal the String walk"
8147 );
8148 }
8149
8150 #[test]
8159 #[ignore = "paired A/B benchmark; run --release with --ignored --nocapture"]
8160 fn thp6w_s5_int_storage_construction_ab() {
8161 use super::multigraph_storage::MgIntStorageProto;
8162 use std::collections::HashMap;
8163 use std::hint::black_box;
8164 use std::time::Instant;
8165
8166 fn push_edge(
8167 stream: &mut Vec<(usize, usize, usize)>,
8168 key_counter: &mut HashMap<(usize, usize), usize>,
8169 u: usize,
8170 v: usize,
8171 ) {
8172 let entry = key_counter.entry((u.min(v), u.max(v))).or_insert(0);
8173 stream.push((u, v, *entry));
8174 *entry += 1;
8175 }
8176
8177 let n = 20000usize;
8178 let mut stream = Vec::new();
8179 let mut key_counter = HashMap::new();
8180 for i in 0..n {
8181 push_edge(&mut stream, &mut key_counter, i, (i + 1) % n);
8182 }
8183 let mut state = 0x2545_F491_4F6C_DD1D_u64;
8185 let draw = |state: &mut u64| -> usize {
8186 *state = state
8187 .wrapping_mul(6_364_136_223_846_793_005)
8188 .wrapping_add(1_442_695_040_888_963_407);
8189 usize::try_from(*state >> 33).unwrap() % n
8190 };
8191 for _ in 0..3 * n {
8192 let u = draw(&mut state);
8193 let v = draw(&mut state);
8194 push_edge(&mut stream, &mut key_counter, u, v);
8195 }
8196 for i in (0..2000).step_by(2) {
8197 push_edge(&mut stream, &mut key_counter, i, i + 1);
8198 }
8199
8200 let time_current = |stream: &[(usize, usize, usize)]| -> f64 {
8201 let t0 = Instant::now();
8202 let mut g = MultiGraph::strict();
8203 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
8204 let dt = t0.elapsed().as_secs_f64();
8205 black_box(&g);
8206 dt
8207 };
8208 let time_proto = |stream: &[(usize, usize, usize)]| -> f64 {
8209 let t0 = Instant::now();
8210 let proto = MgIntStorageProto::bulk_load_int_prefix(n, stream.iter().copied());
8211 let dt = t0.elapsed().as_secs_f64();
8212 black_box(&proto);
8213 dt
8214 };
8215 let time_compact = |stream: &[(usize, usize, usize)]| -> f64 {
8217 let t0 = Instant::now();
8218 let compact = super::multigraph_storage::MgIntStorageProtoCompact::bulk_load_int_prefix(
8219 n,
8220 stream.iter().copied(),
8221 );
8222 let dt = t0.elapsed().as_secs_f64();
8223 black_box(&compact);
8224 dt
8225 };
8226
8227 let mut g = MultiGraph::strict();
8230 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
8231 let proto = MgIntStorageProto::bulk_load_int_prefix(n, stream.iter().copied());
8232 let compact = super::multigraph_storage::MgIntStorageProtoCompact::bulk_load_int_prefix(
8233 n,
8234 stream.iter().copied(),
8235 );
8236 g.with_int_adjacency(|adj| {
8237 for (i, row) in adj.iter().enumerate() {
8238 let proto_row: Vec<usize> = proto.rows[i].keys().copied().collect();
8239 assert_eq!(row, &proto_row, "derived integer row {i} must agree");
8240 let compact_row: Vec<usize> = compact.rows[i].keys().copied().collect();
8241 assert_eq!(row, &compact_row, "compact integer row {i} must agree");
8242 }
8243 });
8244 assert_eq!(
8245 proto.edge_count,
8246 g.edge_count(),
8247 "A/B arms built different graphs"
8248 );
8249 assert_eq!(
8250 compact.edge_count,
8251 g.edge_count(),
8252 "compact arm built a different graph"
8253 );
8254 let time_slab = |stream: &[(usize, usize, usize)]| -> f64 {
8256 let t0 = Instant::now();
8257 let slab = super::multigraph_storage::MgSlabStorage::bulk_load_int_prefix(
8258 n,
8259 stream.iter().copied(),
8260 );
8261 let dt = t0.elapsed().as_secs_f64();
8262 black_box(&slab);
8263 dt
8264 };
8265 let slab = super::multigraph_storage::MgSlabStorage::bulk_load_int_prefix(
8266 n,
8267 stream.iter().copied(),
8268 );
8269 g.with_int_adjacency(|adj| {
8270 for (i, row) in adj.iter().enumerate() {
8271 let slab_row: Vec<usize> = slab.rows[i].keys().copied().collect();
8272 assert_eq!(row, &slab_row, "slab integer row {i} must agree");
8273 }
8274 });
8275 assert_eq!(
8276 slab.edge_count,
8277 g.edge_count(),
8278 "slab arm built a different graph"
8279 );
8280
8281 let median = |samples: &mut Vec<f64>| -> f64 {
8282 samples.sort_by(f64::total_cmp);
8283 samples[samples.len() / 2]
8284 };
8285 let (mut cur, mut pro, mut com, mut sla, mut nul) =
8286 (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new());
8287 for _ in 0..9 {
8288 cur.push(time_current(&stream));
8289 pro.push(time_proto(&stream));
8290 com.push(time_compact(&stream));
8291 sla.push(time_slab(&stream));
8292 nul.push(time_current(&stream));
8293 }
8294 let (mc, mp, mk, ms, mn) = (
8295 median(&mut cur),
8296 median(&mut pro),
8297 median(&mut com),
8298 median(&mut sla),
8299 median(&mut nul),
8300 );
8301 println!(
8302 "THP6W_S5_AB n={n} m={} current={mc:.6}s proto={mp:.6}s compact={mk:.6}s slab={ms:.6}s ratio_current_over_proto={:.3} ratio_current_over_compact={:.3} ratio_current_over_slab={:.3} ratio_compact_over_slab={:.3} null_current_over_current={:.3}",
8303 stream.len(),
8304 mc / mp,
8305 mc / mk,
8306 mc / ms,
8307 mk / ms,
8308 mc / mn,
8309 );
8310 }
8311
8312 #[test]
8313 fn thp6w_int_adjacency_cache_is_fresh_per_clone() {
8314 let mut g = MultiGraph::strict();
8315 for i in 0..4 {
8316 g.add_node(format!("n{i}"));
8317 }
8318 let _ = g.add_edge("n0", "n1");
8319 assert_int_adjacency_matches(&g);
8321 let mut clone = g.clone();
8324 let _ = clone.add_edge("n2", "n3");
8325 assert_int_adjacency_matches(&clone);
8326 assert_int_adjacency_matches(&g);
8327 let _ = g.add_edge("n0", "n2");
8329 assert_int_adjacency_matches(&g);
8330 assert_int_adjacency_matches(&clone);
8331 }
8332
8333 fn single_attr(key: &str, value: &str) -> AttrMap {
8334 let mut attrs = AttrMap::new();
8335 attrs.insert(key.to_owned(), CgseValue::from(value));
8336 attrs
8337 }
8338
8339 #[test]
8340 fn edge_slot_rows_follow_adj_order_and_refresh_after_mutation() {
8341 let mut graph = Graph::strict();
8342 let weighted = |value| {
8343 let mut attrs = AttrMap::new();
8344 attrs.insert("weight".to_owned(), CgseValue::Int(value));
8345 attrs
8346 };
8347 graph
8348 .add_edge_with_attrs("a", "b", weighted(1))
8349 .expect("edge add should succeed");
8350 graph
8351 .add_edge_with_attrs("a", "c", weighted(2))
8352 .expect("edge add should succeed");
8353 graph
8354 .add_edge_with_attrs("b", "c", weighted(3))
8355 .expect("edge add should succeed");
8356
8357 let a = graph.get_node_index("a").expect("a exists");
8358 graph.with_edge_slots_by_neighbor(|rows| {
8359 let weights = rows[a]
8360 .iter()
8361 .map(|&slot| {
8362 graph
8363 .edge_attrs_by_slot(slot)
8364 .and_then(|attrs| attrs.get("weight"))
8365 })
8366 .collect::<Vec<_>>();
8367 assert_eq!(
8368 weights,
8369 vec![Some(&CgseValue::Int(1)), Some(&CgseValue::Int(2))]
8370 );
8371 });
8372
8373 graph.apply_row_orders(&[("a".to_owned(), vec!["c".to_owned(), "b".to_owned()])]);
8374 graph.with_edge_slots_by_neighbor(|rows| {
8375 let weights = rows[a]
8376 .iter()
8377 .map(|&slot| {
8378 graph
8379 .edge_attrs_by_slot(slot)
8380 .and_then(|attrs| attrs.get("weight"))
8381 })
8382 .collect::<Vec<_>>();
8383 assert_eq!(
8384 weights,
8385 vec![Some(&CgseValue::Int(2)), Some(&CgseValue::Int(1))]
8386 );
8387 });
8388
8389 assert!(graph.remove_edge("a", "c"));
8390 graph.with_edge_slots_by_neighbor(|rows| {
8391 assert_eq!(rows[a].len(), 1);
8392 assert_eq!(
8393 graph
8394 .edge_attrs_by_slot(rows[a][0])
8395 .and_then(|attrs| attrs.get("weight")),
8396 Some(&CgseValue::Int(1))
8397 );
8398 });
8399 }
8400
8401 #[test]
8402 fn edges_ordered_tracks_node_and_neighbor_iteration_order() {
8403 let mut graph = Graph::strict();
8404 graph
8405 .add_edge_with_attrs("a", "b", AttrMap::new())
8406 .expect("edge add should succeed");
8407 graph
8408 .add_edge_with_attrs("b", "c", AttrMap::new())
8409 .expect("edge add should succeed");
8410 graph
8411 .add_edge_with_attrs("a", "c", AttrMap::new())
8412 .expect("edge add should succeed");
8413
8414 let pairs = graph
8415 .edges_ordered()
8416 .into_iter()
8417 .map(|edge| (edge.left, edge.right))
8418 .collect::<Vec<(String, String)>>();
8419 assert_eq!(
8420 pairs,
8421 vec![
8422 ("a".to_owned(), "b".to_owned()),
8423 ("a".to_owned(), "c".to_owned()),
8424 ("b".to_owned(), "c".to_owned()),
8425 ]
8426 );
8427 }
8428
8429 #[test]
8430 fn edges_ordered_preserves_traversed_endpoint_orientation() {
8431 let mut graph = Graph::strict();
8432 graph
8433 .add_edge_with_attrs("2", "1", AttrMap::new())
8434 .expect("edge add should succeed");
8435
8436 let pairs = graph
8437 .edges_ordered()
8438 .into_iter()
8439 .map(|edge| (edge.left, edge.right))
8440 .collect::<Vec<(String, String)>>();
8441 assert_eq!(pairs, vec![("2".to_owned(), "1".to_owned())]);
8442
8443 let borrowed_pairs = graph
8444 .edges_ordered_borrowed()
8445 .into_iter()
8446 .map(|(left, right, _)| (left.to_owned(), right.to_owned()))
8447 .collect::<Vec<(String, String)>>();
8448 assert_eq!(borrowed_pairs, vec![("2".to_owned(), "1".to_owned())]);
8449 }
8450
8451 #[test]
8452 fn edges_ordered_uses_node_order_for_preexisting_nodes() {
8453 let mut graph = Graph::strict();
8454 graph.add_node("1".to_owned());
8455 graph.add_node("2".to_owned());
8456 graph
8457 .add_edge_with_attrs("2", "1", AttrMap::new())
8458 .expect("edge add should succeed");
8459
8460 let pairs = graph
8461 .edges_ordered()
8462 .into_iter()
8463 .map(|edge| (edge.left, edge.right))
8464 .collect::<Vec<(String, String)>>();
8465 assert_eq!(pairs, vec![("1".to_owned(), "2".to_owned())]);
8466 }
8467
8468 #[test]
8469 fn edge_storage_order_index_iter_tracks_mutations() {
8470 let mut graph = Graph::strict();
8471 graph.add_node("c");
8472 graph.add_node("a");
8473 graph.add_node("b");
8474 graph
8475 .add_edge_with_attrs("c", "a", single_attr("weight", "1"))
8476 .expect("edge add should succeed");
8477 graph
8478 .add_edge_with_attrs("b", "c", single_attr("weight", "2"))
8479 .expect("edge add should succeed");
8480 graph
8481 .add_edge_with_attrs("a", "c", single_attr("color", "red"))
8482 .expect("duplicate edge update should succeed");
8483
8484 let indexed_edges = |graph: &Graph| {
8485 graph
8486 .edges_storage_order_index_iter()
8487 .map(|(left, right, attrs)| {
8488 (
8489 graph
8490 .get_node_name(left)
8491 .expect("left endpoint index should resolve")
8492 .to_owned(),
8493 graph
8494 .get_node_name(right)
8495 .expect("right endpoint index should resolve")
8496 .to_owned(),
8497 attrs.get("weight").cloned(),
8498 )
8499 })
8500 .collect::<Vec<_>>()
8501 };
8502
8503 assert_eq!(
8504 indexed_edges(&graph),
8505 vec![
8506 (
8507 "a".to_owned(),
8508 "c".to_owned(),
8509 Some(CgseValue::String("1".to_owned())),
8510 ),
8511 (
8512 "b".to_owned(),
8513 "c".to_owned(),
8514 Some(CgseValue::String("2".to_owned())),
8515 ),
8516 ]
8517 );
8518
8519 assert!(graph.remove_edge("a", "c"));
8520 assert_eq!(
8521 indexed_edges(&graph),
8522 vec![(
8523 "b".to_owned(),
8524 "c".to_owned(),
8525 Some(CgseValue::String("2".to_owned())),
8526 )]
8527 );
8528
8529 assert!(graph.remove_node("b"));
8530 assert!(indexed_edges(&graph).is_empty());
8531 }
8532
8533 fn assert_graph_core_invariants(graph: &Graph) {
8534 let mut unique_edges = BTreeSet::new();
8535 for node in graph.nodes_ordered() {
8536 let neighbors = graph
8537 .neighbors(node)
8538 .expect("graph nodes should always have adjacency buckets");
8539 for neighbor in neighbors {
8540 assert!(graph.has_node(neighbor));
8541 assert!(graph.has_edge(node, neighbor));
8542 let reverse_neighbors = graph
8543 .neighbors(neighbor)
8544 .expect("neighbor should always have adjacency bucket");
8545 assert!(reverse_neighbors.contains(&node));
8546 unique_edges.insert(canonical_edge(node, neighbor));
8547 }
8548 }
8549 assert_eq!(graph.edge_count(), unique_edges.len());
8550 }
8551
8552 fn assert_multigraph_core_invariants(graph: &MultiGraph) {
8553 let mut edge_instances = BTreeSet::new();
8554 for node in graph.nodes_ordered() {
8555 let neighbors = graph
8556 .neighbors(node)
8557 .expect("multigraph nodes should always have adjacency buckets");
8558 for neighbor in neighbors {
8559 assert!(graph.has_node(neighbor));
8560 assert!(graph.has_edge(node, neighbor));
8561 let reverse_neighbors = graph
8562 .neighbors(neighbor)
8563 .expect("neighbor should always have adjacency bucket");
8564 assert!(reverse_neighbors.contains(&node));
8565 let keys = graph
8566 .edge_keys(node, neighbor)
8567 .expect("parallel edge bucket should exist");
8568 for key in keys {
8569 let canonical = canonical_edge(node, neighbor);
8570 edge_instances.insert((canonical.0, canonical.1, key));
8571 }
8572 }
8573 }
8574 assert_eq!(graph.edge_count(), edge_instances.len());
8575 }
8576
8577 fn assert_decision_record_schema(record: &DecisionRecord, expected_mode: CompatibilityMode) {
8578 assert!(record.ts_unix_ms > 0);
8579 assert!(!record.operation.trim().is_empty());
8580 assert_eq!(record.mode, expected_mode);
8581 assert!((0.0..=1.0).contains(&record.incompatibility_probability));
8582 assert!(!record.rationale.trim().is_empty());
8583 assert!(!record.evidence.is_empty());
8584 for term in &record.evidence {
8585 assert!(!term.signal.trim().is_empty());
8586 assert!(!term.observed_value.trim().is_empty());
8587 }
8588 }
8589
8590 #[test]
8591 fn add_edge_autocreates_nodes_and_preserves_order() {
8592 let mut graph = Graph::strict();
8593 graph
8594 .add_edge_with_attrs("a", "b", AttrMap::new())
8595 .expect("edge insert should succeed");
8596 graph
8597 .add_edge_with_attrs("a", "c", AttrMap::new())
8598 .expect("edge insert should succeed");
8599
8600 assert_eq!(graph.node_count(), 3);
8601 assert_eq!(graph.edge_count(), 2);
8602 assert_eq!(graph.nodes_ordered(), vec!["a", "b", "c"]);
8603 assert_eq!(graph.neighbors("a"), Some(vec!["b", "c"]));
8604 }
8605
8606 #[test]
8607 fn add_edge_empty_endpoint_resolution_preserves_autocreation_effects() {
8608 let mut graph = Graph::strict();
8609 let before = graph.revision();
8610
8611 graph
8612 .add_edge("c", "a")
8613 .expect("fresh endpoints should be autocreated");
8614
8615 assert_eq!(graph.nodes_ordered(), vec!["c", "a"]);
8619 assert_eq!(graph.neighbors("c"), Some(vec!["a"]));
8620 assert_eq!(graph.neighbors("a"), Some(vec!["c"]));
8621 assert_eq!(graph.revision(), before + 3);
8622
8623 graph
8624 .add_edge("a", "c")
8625 .expect("an existing empty edge is a no-op");
8626 assert_eq!(graph.nodes_ordered(), vec!["c", "a"]);
8627 assert_eq!(graph.revision(), before + 3);
8628 }
8629
8630 #[test]
8631 fn extend_edges_unrecorded_preserves_adjacency_and_records_once() {
8632 let mut graph = Graph::strict();
8633 graph.add_node("a");
8634 graph.add_node("b");
8635 graph.add_node("c");
8636 let before = graph.evidence_ledger().records().len();
8637
8638 let inserted = graph.extend_edges_unrecorded([("a", "b"), ("a", "c"), ("b", "a")]);
8639
8640 assert_eq!(inserted, 2);
8641 assert_eq!(graph.edge_count(), 2);
8642 assert_eq!(graph.neighbors("a"), Some(vec!["b", "c"]));
8643 let records = graph.evidence_ledger().records();
8644 assert_eq!(records.len(), before + 1);
8645 assert_eq!(
8646 records.last().map(|record| record.operation.as_ref()),
8647 Some("extend_edges_unrecorded")
8648 );
8649 }
8650
8651 #[test]
8652 fn extend_existing_index_edges_unrecorded_matches_string_batch_on_int_prefix() {
8653 let nodes = (0..12).map(|node| node.to_string()).collect::<Vec<_>>();
8654 let edge_indices = vec![(10usize, 2usize), (2, 10), (3, 3), (1, 11), (11, 1)];
8655 let edge_strings = edge_indices
8656 .iter()
8657 .map(|(left, right)| (left.to_string(), right.to_string()))
8658 .collect::<Vec<_>>();
8659
8660 let mut by_string = Graph::strict();
8661 let mut by_index = Graph::strict();
8662 let _ = by_string.extend_nodes_unrecorded(nodes.clone());
8663 let _ = by_index.extend_nodes_unrecorded(nodes);
8664
8665 assert!(by_index.nodes_are_contiguous_int_prefix());
8666 assert_eq!(by_string.extend_edges_unrecorded(edge_strings), 3);
8667 assert_eq!(
8668 by_index.extend_existing_index_edges_unrecorded(edge_indices),
8669 3
8670 );
8671
8672 assert_eq!(by_index.snapshot(), by_string.snapshot());
8673 assert_graph_core_invariants(&by_index);
8674 }
8675
8676 #[test]
8677 fn extend_edges_with_attrs_unrecorded_matches_add_edge_with_attrs() {
8678 let mut attrs1 = AttrMap::new();
8683 attrs1.insert("w".to_owned(), CgseValue::Int(1));
8684 let mut attrs2 = AttrMap::new();
8685 attrs2.insert("w".to_owned(), CgseValue::Int(7));
8686 attrs2.insert("c".to_owned(), CgseValue::String("x".to_owned()));
8687
8688 let mut reference = Graph::strict();
8689 reference
8690 .add_edge_with_attrs("a", "b", attrs1.clone())
8691 .unwrap();
8692 reference
8693 .add_edge_with_attrs("b", "c", AttrMap::new())
8694 .unwrap();
8695 reference
8697 .add_edge_with_attrs("b", "a", attrs2.clone())
8698 .unwrap();
8699 reference
8701 .add_edge_with_attrs("d", "d", attrs1.clone())
8702 .unwrap();
8703
8704 let mut bulk = Graph::strict();
8705 let before = bulk.evidence_ledger().records().len();
8706 let inserted = bulk.extend_edges_with_attrs_unrecorded([
8707 ("a".to_owned(), "b".to_owned(), attrs1.clone()),
8708 ("b".to_owned(), "c".to_owned(), AttrMap::new()),
8709 ("b".to_owned(), "a".to_owned(), attrs2.clone()),
8710 ("d".to_owned(), "d".to_owned(), attrs1.clone()),
8711 ]);
8712
8713 assert_eq!(inserted, 3);
8714 assert_eq!(bulk.edge_count(), reference.edge_count());
8715 assert_eq!(bulk.nodes_ordered(), reference.nodes_ordered());
8716 for node in bulk.nodes_ordered() {
8717 assert_eq!(bulk.neighbors(node), reference.neighbors(node));
8718 }
8719 assert_eq!(bulk.edge_attrs("a", "b"), reference.edge_attrs("a", "b"));
8720 assert_eq!(bulk.edge_attrs("d", "d"), reference.edge_attrs("d", "d"));
8721 assert_eq!(bulk.evidence_ledger().records().len(), before + 1);
8723 }
8724
8725 #[test]
8726 fn multigraph_int_prefix_keyed_edges_match_string_batch() {
8727 let indexed_edges = vec![
8728 (0usize, 1usize, 0usize),
8729 (1, 2, 0),
8730 (2, 0, 0),
8731 (0, 1, 1),
8732 (2, 2, 0),
8733 ];
8734 let string_edges = indexed_edges
8735 .iter()
8736 .map(|(left, right, key)| (left.to_string(), right.to_string(), *key, AttrMap::new()))
8737 .collect::<Vec<_>>();
8738
8739 let mut by_string = MultiGraph::strict();
8740 let mut by_index = MultiGraph::strict();
8741
8742 assert_eq!(
8743 by_string.extend_keyed_edges_with_attrs_unrecorded(string_edges),
8744 indexed_edges.len()
8745 );
8746 assert_eq!(
8747 by_index.extend_fresh_int_prefix_keyed_edges_unrecorded(3, indexed_edges),
8748 5
8749 );
8750
8751 assert_eq!(by_index.snapshot(), by_string.snapshot());
8752 assert_multigraph_core_invariants(&by_index);
8753 }
8754
8755 #[test]
8756 fn extend_nodes_unrecorded_preserves_order_and_records_once() {
8757 let mut graph = Graph::strict();
8758 graph.add_node("1");
8759 let before = graph.evidence_ledger().records().len();
8760
8761 let inserted =
8762 graph.extend_nodes_unrecorded(["0".to_owned(), "1".to_owned(), "2".to_owned()]);
8763
8764 assert_eq!(inserted, 2);
8765 assert_eq!(graph.node_count(), 3);
8766 assert_eq!(graph.nodes_ordered(), vec!["1", "0", "2"]);
8767 assert_eq!(graph.neighbor_count("0"), 0);
8768 assert_eq!(graph.neighbor_count("2"), 0);
8769 let records = graph.evidence_ledger().records();
8770 assert_eq!(records.len(), before + 1);
8771 assert_eq!(
8772 records.last().map(|record| record.operation.as_ref()),
8773 Some("extend_nodes_unrecorded")
8774 );
8775 }
8776
8777 #[test]
8778 fn neighbors_iter_preserves_deterministic_order() {
8779 let mut graph = Graph::strict();
8780 graph.add_edge("a", "b").expect("edge add should succeed");
8781 graph.add_edge("a", "c").expect("edge add should succeed");
8782 graph.add_edge("a", "d").expect("edge add should succeed");
8783
8784 let neighbors = graph
8785 .neighbors_iter("a")
8786 .expect("neighbors should exist")
8787 .collect::<Vec<&str>>();
8788 assert_eq!(neighbors, vec!["b", "c", "d"]);
8789 }
8790
8791 #[test]
8792 fn neighbor_count_matches_neighbors_len() {
8793 let mut graph = Graph::strict();
8794 graph.add_edge("a", "b").expect("edge add should succeed");
8795 graph.add_edge("a", "c").expect("edge add should succeed");
8796 assert_eq!(graph.neighbor_count("a"), 2);
8797 assert_eq!(graph.neighbor_count("missing"), 0);
8798 }
8799
8800 #[test]
8801 fn repeated_edge_updates_attrs_in_place() {
8802 let mut graph = Graph::strict();
8803 let mut first = AttrMap::new();
8804 first.insert("weight".to_owned(), "1".into());
8805 graph
8806 .add_edge_with_attrs("a", "b", first)
8807 .expect("edge insert should succeed");
8808
8809 let mut second = AttrMap::new();
8810 second.insert("color".to_owned(), "blue".into());
8811 graph
8812 .add_edge_with_attrs("b", "a", second)
8813 .expect("edge update should succeed");
8814
8815 let attrs = graph
8816 .edge_attrs("a", "b")
8817 .expect("edge attrs should be present");
8818 assert_eq!(
8819 attrs.get("weight"),
8820 Some(&CgseValue::String("1".to_owned()))
8821 );
8822 assert_eq!(
8823 attrs.get("color"),
8824 Some(&CgseValue::String("blue".to_owned()))
8825 );
8826 assert_eq!(graph.edge_count(), 1);
8827 }
8828
8829 #[test]
8830 fn complete_graph_bulk_constructor_preserves_ordered_contract() {
8831 let graph = Graph::complete_graph(CompatibilityMode::Strict, 5);
8832
8833 assert_eq!(graph.node_count(), 5);
8834 assert_eq!(graph.edge_count(), 10);
8835 assert_eq!(graph.nodes_ordered(), vec!["0", "1", "2", "3", "4"]);
8836 assert_eq!(
8837 graph.neighbors("0").expect("node 0 should exist"),
8838 vec!["1", "2", "3", "4"]
8839 );
8840 assert_eq!(
8841 graph.neighbors("4").expect("node 4 should exist"),
8842 vec!["0", "1", "2", "3"]
8843 );
8844 assert_eq!(
8845 graph
8846 .edges_ordered()
8847 .into_iter()
8848 .map(|edge| (edge.left, edge.right))
8849 .collect::<Vec<_>>(),
8850 vec![
8851 ("0".to_owned(), "1".to_owned()),
8852 ("0".to_owned(), "2".to_owned()),
8853 ("0".to_owned(), "3".to_owned()),
8854 ("0".to_owned(), "4".to_owned()),
8855 ("1".to_owned(), "2".to_owned()),
8856 ("1".to_owned(), "3".to_owned()),
8857 ("1".to_owned(), "4".to_owned()),
8858 ("2".to_owned(), "3".to_owned()),
8859 ("2".to_owned(), "4".to_owned()),
8860 ("3".to_owned(), "4".to_owned()),
8861 ]
8862 );
8863
8864 let operations = graph
8865 .evidence_ledger()
8866 .records()
8867 .iter()
8868 .map(|record| record.operation.as_ref())
8869 .collect::<Vec<_>>();
8870 assert_eq!(operations, vec!["complete_graph_bulk"]);
8871 }
8872
8873 #[test]
8874 fn remove_node_removes_incident_edges() {
8875 let mut graph = Graph::strict();
8876 graph.add_edge("a", "b").expect("edge add should succeed");
8877 graph.add_edge("b", "c").expect("edge add should succeed");
8878 assert!(graph.remove_node("b"));
8879 assert_eq!(graph.node_count(), 2);
8880 assert_eq!(graph.edge_count(), 0);
8881 }
8882
8883 #[test]
8894 #[ignore = "measurement; run with --release --ignored --nocapture"]
8895 fn ledger_record_cost_ab() {
8896 use fnx_runtime::{EvidenceTerm, RuntimePolicy, bool_evidence, count_evidence};
8897 use std::hint::black_box;
8898 use std::time::Instant;
8899
8900 const EDGES: usize = 20_000;
8901 const ROUNDS: usize = 21;
8902 const RATIONALE: &str = "argmin expected loss over {allow,full_validate,fail_closed}";
8903
8904 fn gate_terms() -> Vec<EvidenceTerm> {
8907 vec![EvidenceTerm {
8908 signal: "unknown_incompatible_feature".into(),
8909 observed_value: bool_evidence(false),
8910 log_likelihood_ratio: 12.0,
8911 }]
8912 }
8913 fn outcome_terms(edge_attr_count: usize) -> Vec<EvidenceTerm> {
8914 vec![
8915 EvidenceTerm {
8916 signal: "self_loop".into(),
8917 observed_value: bool_evidence(false),
8918 log_likelihood_ratio: -0.5,
8919 },
8920 EvidenceTerm {
8921 signal: "edge_attr_count".into(),
8922 observed_value: count_evidence(edge_attr_count),
8923 log_likelihood_ratio: -2.0,
8924 },
8925 EvidenceTerm {
8926 signal: "left_autocreated".into(),
8927 observed_value: bool_evidence(true),
8928 log_likelihood_ratio: -1.25,
8929 },
8930 EvidenceTerm {
8931 signal: "right_autocreated".into(),
8932 observed_value: bool_evidence(true),
8933 log_likelihood_ratio: -1.25,
8934 },
8935 ]
8936 }
8937
8938 fn node_terms() -> Vec<EvidenceTerm> {
8944 vec![
8945 EvidenceTerm {
8946 signal: "node_preexisting".into(),
8947 observed_value: bool_evidence(false),
8948 log_likelihood_ratio: -3.0,
8949 },
8950 EvidenceTerm {
8951 signal: "attrs_count".into(),
8952 observed_value: count_evidence(0),
8953 log_likelihood_ratio: -1.0,
8954 },
8955 ]
8956 }
8957
8958 let recorded = || {
8960 let mut policy = RuntimePolicy::strict();
8961 let start = Instant::now();
8962 for index in 0..EDGES {
8963 let action = policy.action_for(0.08, false);
8964 policy.record("add_edge", action, 0.08, RATIONALE, gate_terms());
8965 policy.record(
8966 "add_edge",
8967 action,
8968 0.08,
8969 RATIONALE,
8970 outcome_terms(index & 3),
8971 );
8972 }
8973 let elapsed = start.elapsed();
8974 assert_eq!(policy.decision_log().total_recorded(), (EDGES * 2) as u64);
8975 elapsed
8976 };
8977
8978 let recorded_single = || {
8982 let mut policy = RuntimePolicy::strict();
8983 let start = Instant::now();
8984 for index in 0..EDGES {
8985 let action = policy.action_for(0.08, false);
8986 policy.record(
8987 "add_edge",
8988 action,
8989 0.08,
8990 RATIONALE,
8991 outcome_terms(index & 3),
8992 );
8993 }
8994 let elapsed = start.elapsed();
8995 assert_eq!(policy.decision_log().total_recorded(), EDGES as u64);
8996 elapsed
8997 };
8998
8999 let recorded_edge_build = || {
9007 let mut policy = RuntimePolicy::strict();
9008 let start = Instant::now();
9009 for index in 0..EDGES {
9010 let node_action = policy.action_for(0.0, false);
9011 policy.record("add_node", node_action, 0.0, RATIONALE, node_terms());
9012 let action = policy.action_for(0.08, false);
9013 policy.record(
9014 "add_edge",
9015 action,
9016 0.08,
9017 RATIONALE,
9018 outcome_terms(index & 3),
9019 );
9020 }
9021 let elapsed = start.elapsed();
9022 assert_eq!(policy.decision_log().total_recorded(), (EDGES * 2) as u64);
9023 elapsed
9024 };
9025
9026 let unbounded = || {
9030 let policy = RuntimePolicy::strict();
9031 let mode = policy.mode();
9032 let mut records: Vec<DecisionRecord> = Vec::new();
9033 let start = Instant::now();
9034 for index in 0..EDGES {
9035 let action = policy.action_for(0.08, false);
9036 records.push(DecisionRecord {
9037 ts_unix_ms: fnx_runtime::unix_time_ms(),
9038 operation: "add_edge".into(),
9039 mode,
9040 action,
9041 incompatibility_probability: 0.08,
9042 rationale: RATIONALE.into(),
9043 evidence: gate_terms(),
9044 });
9045 records.push(DecisionRecord {
9046 ts_unix_ms: fnx_runtime::unix_time_ms(),
9047 operation: "add_edge".into(),
9048 mode,
9049 action,
9050 incompatibility_probability: 0.08,
9051 rationale: RATIONALE.into(),
9052 evidence: outcome_terms(index & 3),
9053 });
9054 }
9055 let elapsed = start.elapsed();
9056 assert_eq!(records.len(), EDGES * 2);
9057 elapsed
9058 };
9059
9060 let discarded = || {
9062 let policy = RuntimePolicy::strict();
9063 let start = Instant::now();
9064 for index in 0..EDGES {
9065 let action = policy.action_for(0.08, false);
9066 black_box((action, "add_edge", 0.08, RATIONALE, gate_terms()));
9067 black_box((
9068 action,
9069 "add_edge",
9070 0.08,
9071 RATIONALE,
9072 outcome_terms(index & 3),
9073 ));
9074 }
9075 let elapsed = start.elapsed();
9076 assert!(policy.decision_log().records().is_empty());
9077 elapsed
9078 };
9079
9080 let clocked = || {
9082 let policy = RuntimePolicy::strict();
9083 let start = Instant::now();
9084 for index in 0..EDGES {
9085 let action = policy.action_for(0.08, false);
9086 black_box((action, fnx_runtime::unix_time_ms(), gate_terms()));
9087 black_box((
9088 action,
9089 fnx_runtime::unix_time_ms(),
9090 outcome_terms(index & 3),
9091 ));
9092 }
9093 start.elapsed()
9094 };
9095
9096 let materialized = || {
9100 let policy = RuntimePolicy::strict();
9101 let mode = policy.mode();
9102 let start = Instant::now();
9103 for index in 0..EDGES {
9104 let action = policy.action_for(0.08, false);
9105 black_box(DecisionRecord {
9106 ts_unix_ms: fnx_runtime::unix_time_ms(),
9107 operation: "add_edge".into(),
9108 mode,
9109 action,
9110 incompatibility_probability: 0.08,
9111 rationale: RATIONALE.into(),
9112 evidence: gate_terms(),
9113 });
9114 black_box(DecisionRecord {
9115 ts_unix_ms: fnx_runtime::unix_time_ms(),
9116 operation: "add_edge".into(),
9117 mode,
9118 action,
9119 incompatibility_probability: 0.08,
9120 rationale: RATIONALE.into(),
9121 evidence: outcome_terms(index & 3),
9122 });
9123 }
9124 start.elapsed()
9125 };
9126
9127 let add_edge_full = || {
9129 let mut graph = Graph::strict();
9130 let keys = (0..=EDGES).map(|n| n.to_string()).collect::<Vec<_>>();
9131 let start = Instant::now();
9132 for index in 0..EDGES {
9133 graph
9134 .add_edge_with_attrs(
9135 keys[index].clone(),
9136 keys[index + 1].clone(),
9137 AttrMap::new(),
9138 )
9139 .expect("edge add should succeed");
9140 }
9141 let elapsed = start.elapsed();
9142 assert_eq!(graph.edge_count(), EDGES);
9143 elapsed
9144 };
9145
9146 let mut record_ns = Vec::with_capacity(ROUNDS);
9147 let mut single_ns = Vec::with_capacity(ROUNDS);
9148 let mut edge_build_ns = Vec::with_capacity(ROUNDS);
9149 let mut unbounded_ns = Vec::with_capacity(ROUNDS);
9150 let mut discard_ns = Vec::with_capacity(ROUNDS);
9151 let mut clock_ns = Vec::with_capacity(ROUNDS);
9152 let mut material_ns = Vec::with_capacity(ROUNDS);
9153 let mut full_ns = Vec::with_capacity(ROUNDS);
9154 for _ in 0..ROUNDS {
9155 record_ns.push(recorded().as_nanos() as f64 / EDGES as f64);
9156 single_ns.push(recorded_single().as_nanos() as f64 / EDGES as f64);
9157 edge_build_ns.push(recorded_edge_build().as_nanos() as f64 / EDGES as f64);
9158 unbounded_ns.push(unbounded().as_nanos() as f64 / EDGES as f64);
9159 discard_ns.push(discarded().as_nanos() as f64 / EDGES as f64);
9160 clock_ns.push(clocked().as_nanos() as f64 / EDGES as f64);
9161 material_ns.push(materialized().as_nanos() as f64 / EDGES as f64);
9162 full_ns.push(add_edge_full().as_nanos() as f64 / EDGES as f64);
9163 }
9164 let median = |samples: &mut Vec<f64>| {
9165 samples.sort_by(f64::total_cmp);
9166 samples[samples.len() / 2]
9167 };
9168 let recorded_ns = median(&mut record_ns);
9169 let single_median_ns = median(&mut single_ns);
9170 let edge_build_median_ns = median(&mut edge_build_ns);
9171 let unbounded_median_ns = median(&mut unbounded_ns);
9172 let discarded_ns = median(&mut discard_ns);
9173 let clocked_ns = median(&mut clock_ns);
9174 let materialized_ns = median(&mut material_ns);
9175 let full = median(&mut full_ns);
9176 println!("ledger_record_cost_ab n={EDGES} rounds={ROUNDS} (2 records/edge)");
9177 println!(" A bounded ledger, 2 records: {recorded_ns:8.1} ns/edge");
9178 println!(" A1 bounded ledger, 1 record : {single_median_ns:8.1} ns/edge (SHIPPED)");
9179 println!(
9180 " A2 two records per edge : {edge_build_median_ns:8.1} ns/edge (pre-wa1b9 counterfactual)"
9181 );
9182 println!(" A0 unbounded Vec (incumbent): {unbounded_median_ns:8.1} ns/edge");
9183 println!(" B build terms only : {discarded_ns:8.1} ns/edge");
9184 println!(" C terms + clock : {clocked_ns:8.1} ns/edge");
9185 println!(" D terms + clock + record : {materialized_ns:8.1} ns/edge");
9186 println!(" add_edge_with_attrs (total) : {full:8.1} ns/edge");
9187 println!(
9188 " -> clock (C-B) : {:8.1} ns/edge",
9189 clocked_ns - discarded_ns
9190 );
9191 println!(
9192 " -> static str clones (D-C) : {:8.1} ns/edge",
9193 materialized_ns - clocked_ns
9194 );
9195 println!(
9196 " -> unbounded retention (A0-D): {:8.1} ns/edge",
9197 unbounded_median_ns - materialized_ns
9198 );
9199 println!(
9200 " -> bounded retention (A-D) : {:8.1} ns/edge",
9201 recorded_ns - materialized_ns
9202 );
9203 println!(
9204 " -> bound speedup (A0/A) : {:8.4}x",
9205 unbounded_median_ns / recorded_ns
9206 );
9207 println!(
9208 " -> single-record (A/A1) : {:8.4}x",
9209 recorded_ns / single_median_ns
9210 );
9211 println!(
9212 " -> both levers (A0/A1) : {:8.4}x",
9213 unbounded_median_ns / single_median_ns
9214 );
9215 println!(
9216 " -> ledger share, SHIPPED : {:6.1}% (A1/full — one record per add_edge)",
9217 single_median_ns / full * 100.0
9218 );
9219 println!(
9220 " -> ledger share, pre-wa1b9 : {:6.1}% (A2/full — when autocreation also recorded)",
9221 edge_build_median_ns / full * 100.0
9222 );
9223 }
9224
9225 #[test]
9256 #[ignore = "measurement; run with --release --ignored --nocapture"]
9257 fn ledger_cow_field_cost_ab() {
9258 use fnx_runtime::{
9259 DecisionAction, EvidenceTerm, RuntimePolicy, bool_evidence, count_evidence,
9260 };
9261 use std::borrow::Cow;
9262 use std::hint::black_box;
9263 use std::time::{Duration, Instant};
9264
9265 const EDGES: usize = 20_000;
9266 const ROUNDS: usize = 21;
9267 const CAP: usize = fnx_runtime::DECISION_LEDGER_RETAINED_RECORDS;
9268 const RATIONALE: &str = "argmin expected loss over {allow,full_validate,fail_closed}";
9269
9270 struct CowRecord {
9272 ts_unix_ms: u128,
9273 operation: Cow<'static, str>,
9274 action: DecisionAction,
9275 incompatibility_probability: f64,
9276 rationale: Cow<'static, str>,
9277 evidence: Vec<CowTerm>,
9278 }
9279 struct CowTerm {
9280 signal: Cow<'static, str>,
9281 observed_value: Cow<'static, str>,
9282 log_likelihood_ratio: f64,
9283 }
9284
9285 fn owned_terms(edge_attr_count: usize) -> Vec<EvidenceTerm> {
9289 vec![
9290 EvidenceTerm {
9291 signal: "self_loop".into(),
9292 observed_value: bool_evidence(false),
9293 log_likelihood_ratio: -0.5,
9294 },
9295 EvidenceTerm {
9296 signal: "edge_attr_count".into(),
9297 observed_value: count_evidence(edge_attr_count),
9298 log_likelihood_ratio: -2.0,
9299 },
9300 EvidenceTerm {
9301 signal: "left_autocreated".into(),
9302 observed_value: bool_evidence(true),
9303 log_likelihood_ratio: -1.25,
9304 },
9305 EvidenceTerm {
9306 signal: "right_autocreated".into(),
9307 observed_value: bool_evidence(true),
9308 log_likelihood_ratio: -1.25,
9309 },
9310 ]
9311 }
9312 fn borrowed_terms(edge_attr_count: usize) -> Vec<CowTerm> {
9313 vec![
9314 CowTerm {
9315 signal: Cow::Borrowed("self_loop"),
9316 observed_value: Cow::Borrowed("false"),
9317 log_likelihood_ratio: -0.5,
9318 },
9319 CowTerm {
9320 signal: Cow::Borrowed("edge_attr_count"),
9321 observed_value: Cow::Owned(edge_attr_count.to_string()),
9323 log_likelihood_ratio: -2.0,
9324 },
9325 CowTerm {
9326 signal: Cow::Borrowed("left_autocreated"),
9327 observed_value: Cow::Borrowed("true"),
9328 log_likelihood_ratio: -1.25,
9329 },
9330 CowTerm {
9331 signal: Cow::Borrowed("right_autocreated"),
9332 observed_value: Cow::Borrowed("true"),
9333 log_likelihood_ratio: -1.25,
9334 },
9335 ]
9336 }
9337
9338 let owned_arm = || -> Duration {
9339 let policy = RuntimePolicy::strict();
9340 let mode = policy.mode();
9341 let mut log: Vec<DecisionRecord> = Vec::new();
9342 let start = Instant::now();
9343 for index in 0..EDGES {
9344 let action = policy.action_for(0.08, false);
9345 if log.len() == CAP {
9346 log.clear();
9347 }
9348 log.push(DecisionRecord {
9349 ts_unix_ms: fnx_runtime::unix_time_ms(),
9350 operation: "add_edge".into(),
9351 mode,
9352 action,
9353 incompatibility_probability: 0.08,
9354 rationale: RATIONALE.into(),
9355 evidence: owned_terms(index & 3),
9356 });
9357 }
9358 let elapsed = start.elapsed();
9359 let mut sink = 0usize;
9364 for record in &log {
9365 sink ^= record.operation.len()
9366 ^ record.rationale.len()
9367 ^ (record.ts_unix_ms as usize)
9368 ^ (record.incompatibility_probability.to_bits() as usize)
9369 ^ usize::from(matches!(record.action, DecisionAction::Allow));
9370 for term in &record.evidence {
9371 sink ^= term.signal.len()
9372 ^ term.observed_value.len()
9373 ^ (term.log_likelihood_ratio.to_bits() as usize);
9374 }
9375 }
9376 black_box(sink);
9377 elapsed
9378 };
9379
9380 let borrowed_arm = || -> Duration {
9381 let policy = RuntimePolicy::strict();
9382 let mut log: Vec<CowRecord> = Vec::new();
9383 let start = Instant::now();
9384 for index in 0..EDGES {
9385 let action = policy.action_for(0.08, false);
9386 if log.len() == CAP {
9387 log.clear();
9388 }
9389 log.push(CowRecord {
9390 ts_unix_ms: fnx_runtime::unix_time_ms(),
9391 operation: Cow::Borrowed("add_edge"),
9392 action,
9393 incompatibility_probability: 0.08,
9394 rationale: Cow::Borrowed(RATIONALE),
9395 evidence: borrowed_terms(index & 3),
9396 });
9397 }
9398 let elapsed = start.elapsed();
9399 let mut sink = 0usize;
9400 for record in &log {
9401 sink ^= record.operation.len()
9402 ^ record.rationale.len()
9403 ^ (record.ts_unix_ms as usize)
9404 ^ (record.incompatibility_probability.to_bits() as usize)
9405 ^ usize::from(matches!(record.action, DecisionAction::Allow));
9406 for term in &record.evidence {
9407 sink ^= term.signal.len()
9408 ^ term.observed_value.len()
9409 ^ (term.log_likelihood_ratio.to_bits() as usize);
9410 }
9411 }
9412 black_box(sink);
9413 elapsed
9414 };
9415
9416 let balanced =
9421 |first: &dyn Fn() -> Duration, second: &dyn Fn() -> Duration| -> (f64, f64) {
9422 let a1 = first();
9423 let b1 = second();
9424 let b2 = second();
9425 let a2 = first();
9426 let scale = (2 * EDGES) as f64;
9427 (
9428 (a1 + a2).as_nanos() as f64 / scale,
9429 (b1 + b2).as_nanos() as f64 / scale,
9430 )
9431 };
9432
9433 owned_arm();
9434 borrowed_arm();
9435
9436 let mut owned_ns = Vec::with_capacity(ROUNDS);
9437 let mut cow_ns = Vec::with_capacity(ROUNDS);
9438 let mut null_a_ns = Vec::with_capacity(ROUNDS);
9439 let mut null_b_ns = Vec::with_capacity(ROUNDS);
9440 for _ in 0..ROUNDS {
9441 let (na, nb) = balanced(&owned_arm, &owned_arm);
9442 let (owned, cow) = balanced(&owned_arm, &borrowed_arm);
9443 null_a_ns.push(na);
9444 null_b_ns.push(nb);
9445 owned_ns.push(owned);
9446 cow_ns.push(cow);
9447 }
9448
9449 let median = |samples: &mut Vec<f64>| {
9450 samples.sort_by(f64::total_cmp);
9451 samples[samples.len() / 2]
9452 };
9453 let owned = median(&mut owned_ns);
9454 let cow = median(&mut cow_ns);
9455 let null_a = median(&mut null_a_ns);
9456 let null_b = median(&mut null_b_ns);
9457
9458 println!("ledger_cow_field_cost_ab n={EDGES} rounds={ROUNDS} cap={CAP}");
9459 println!(" S String fields : {owned:8.1} ns/edge (incumbent)");
9460 println!(" C Cow fields : {cow:8.1} ns/edge (proposed)");
9461 println!(" N1 String, null A : {null_a:8.1} ns/edge");
9462 println!(" N2 String, null B : {null_b:8.1} ns/edge");
9463 println!(" -> A/A null (N2/N1): {:8.4}x", null_b / null_a);
9464 println!(" -> Cow gain (S/C) : {:8.4}x", owned / cow);
9465 println!(" -> saving : {:8.1} ns/edge", owned - cow);
9466 }
9467
9468 #[test]
9474 #[ignore]
9475 fn ab_bench_multigraph_remove_node() {
9476 use std::time::Instant;
9477 const N: usize = 1000;
9478 const M: usize = 8000;
9479 const ITERS: usize = 500;
9480 let build = || {
9481 let mut g = MultiGraph::new(CompatibilityMode::Strict);
9482 for i in 0..N {
9483 let _ = g.add_node(i.to_string());
9484 }
9485 let mut s: u64 = 0x9E3779B97F4A7C15;
9486 let mut next = || {
9487 s = s
9488 .wrapping_mul(6364136223846793005)
9489 .wrapping_add(1442695040888963407);
9490 (s >> 33) as usize % N
9491 };
9492 for _ in 0..M {
9493 let a = next();
9494 let b = next();
9495 let _ = g.add_edge(a.to_string(), b.to_string());
9496 }
9497 g
9498 };
9499
9500 let mut gnew = build();
9501 let victims: Vec<String> = gnew
9502 .nodes_ordered()
9503 .into_iter()
9504 .take(ITERS)
9505 .map(str::to_owned)
9506 .collect();
9507 let t = Instant::now();
9508 for node in &victims {
9509 gnew.remove_node(node);
9510 }
9511 let new_t = t.elapsed();
9512
9513 let mut gold = build();
9516 let t = Instant::now();
9517 for node in &victims {
9518 gold.remove_node(node);
9519 }
9520 let null_t = t.elapsed();
9521
9522 assert_eq!(gnew.node_count(), gold.node_count());
9523 assert_eq!(gnew.edge_count(), gold.edge_count());
9524 assert_eq!(gnew.edges_ordered(), gold.edges_ordered());
9525 eprintln!(
9526 "MultiGraph remove_node A/A x{ITERS}: candidate {new_t:?}, null {null_t:?}, ratio={:.3}",
9527 null_t.as_secs_f64() / new_t.as_secs_f64()
9528 );
9529 }
9530
9531 #[test]
9532 fn remove_nodes_from_matches_repeated_removal_and_rebuilds_indices() {
9533 let edges = [
9534 ("0", "1"),
9535 ("1", "2"),
9536 ("2", "3"),
9537 ("3", "4"),
9538 ("4", "5"),
9539 ("0", "5"),
9540 ("1", "4"),
9541 ("2", "5"),
9542 ];
9543 let mut batch = Graph::strict();
9544 let mut repeated = Graph::strict();
9545 for (left, right) in edges {
9546 batch.add_edge(left, right).expect("batch edge add");
9547 repeated.add_edge(left, right).expect("repeated edge add");
9548 }
9549
9550 let victims = ["1", "3", "99", "1"];
9551 let removed = batch.remove_nodes_from(victims);
9552 for victim in victims {
9553 let _ = repeated.remove_node(victim);
9554 }
9555
9556 assert_eq!(removed, (2, 5));
9557 assert_eq!(batch.snapshot(), repeated.snapshot());
9558 let ordered_nodes = batch
9559 .nodes_ordered()
9560 .into_iter()
9561 .map(str::to_owned)
9562 .collect::<Vec<_>>();
9563 for (node_index, node) in ordered_nodes.iter().enumerate() {
9564 let names_from_strings = batch.neighbors(node).expect("node should exist");
9565 let names_from_indices = batch
9566 .neighbors_indices(node_index)
9567 .expect("index should exist")
9568 .iter()
9569 .map(|index| batch.get_node_name(*index).expect("index should resolve"))
9570 .collect::<Vec<_>>();
9571 assert_eq!(names_from_indices, names_from_strings);
9572 }
9573 let names_from_edge_indices = batch
9574 .edges_storage_order_index_iter()
9575 .map(|(left, right, _)| {
9576 (
9577 batch
9578 .get_node_name(left)
9579 .expect("left endpoint should resolve")
9580 .to_owned(),
9581 batch
9582 .get_node_name(right)
9583 .expect("right endpoint should resolve")
9584 .to_owned(),
9585 )
9586 })
9587 .collect::<Vec<_>>();
9588 let names_from_edge_storage = batch
9589 .edges_storage_order_iter()
9590 .map(|(left, right, _)| (left.to_owned(), right.to_owned()))
9591 .collect::<Vec<_>>();
9592 assert_eq!(names_from_edge_indices, names_from_edge_storage);
9593 }
9594
9595 #[test]
9596 fn multigraph_ordered_indices_match_borrowed_edges() {
9597 let mut graph = MultiGraph::strict();
9598 graph
9599 .add_edge_with_key_and_attrs("b", "a", 2, single_attr("w", "left"))
9600 .expect("first keyed edge should be added");
9601 graph
9602 .add_edge_with_key_and_attrs("a", "b", 0, single_attr("w", "right"))
9603 .expect("parallel keyed edge should be added");
9604 graph
9605 .add_edge_with_key_and_attrs("a", "c", 3, AttrMap::new())
9606 .expect("third edge should be added");
9607 graph
9608 .add_edge_with_key_and_attrs("c", "c", 1, single_attr("w", "loop"))
9609 .expect("self-loop should be added");
9610
9611 let nodes = graph.nodes_ordered();
9612 let from_indices = graph
9613 .edges_ordered_indices_borrowed()
9614 .into_iter()
9615 .map(|(left, right, key, attrs)| {
9616 (
9617 nodes[left].to_owned(),
9618 nodes[right].to_owned(),
9619 key,
9620 attrs.clone(),
9621 )
9622 })
9623 .collect::<Vec<_>>();
9624 let from_borrowed = graph
9625 .edges_ordered_borrowed()
9626 .into_iter()
9627 .map(|(left, right, key, attrs)| {
9628 (left.to_owned(), right.to_owned(), key, attrs.clone())
9629 })
9630 .collect::<Vec<_>>();
9631
9632 assert_eq!(from_indices, from_borrowed);
9633 }
9634
9635 #[test]
9636 fn multigraph_node_index_accessors_follow_remove_readd() {
9637 let mut graph = MultiGraph::strict();
9638 for node in ["prefix", "s", "a", "b", "t"] {
9639 let _ = graph.add_node(node);
9640 }
9641
9642 for (expected_index, node) in ["prefix", "s", "a", "b", "t"].into_iter().enumerate() {
9643 assert_eq!(graph.get_node_index(node), Some(expected_index));
9644 assert_eq!(graph.get_node_name(expected_index), Some(node));
9645 }
9646
9647 assert!(graph.remove_node("prefix"));
9648 assert!(graph.remove_node("a"));
9649 let _ = graph.add_node("a");
9650
9651 for (expected_index, node) in ["s", "b", "t", "a"].into_iter().enumerate() {
9652 assert_eq!(graph.get_node_index(node), Some(expected_index));
9653 assert_eq!(graph.get_node_name(expected_index), Some(node));
9654 }
9655 assert_eq!(graph.get_node_index("prefix"), None);
9656 assert_eq!(graph.get_node_name(graph.node_count()), None);
9657 }
9658
9659 #[test]
9660 fn strict_mode_fails_closed_for_unknown_incompatible_feature() {
9661 let mut graph = Graph::strict();
9662 let mut attrs = AttrMap::new();
9663 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
9664 let err = graph
9665 .add_edge_with_attrs("a", "b", attrs)
9666 .expect_err("strict mode should fail closed");
9667
9668 assert_eq!(
9669 err,
9670 GraphError::FailClosed {
9671 operation: "add_edge",
9672 reason: "incompatible edge metadata".to_owned(),
9673 }
9674 );
9675
9676 let last_record = graph
9677 .evidence_ledger()
9678 .records()
9679 .last()
9680 .expect("strict fail-closed path should emit a ledger row");
9681 assert_decision_record_schema(last_record, CompatibilityMode::Strict);
9682 assert_eq!(last_record.operation, "add_edge");
9683 assert_eq!(last_record.action, DecisionAction::FailClosed);
9684 assert!(
9685 last_record
9686 .evidence
9687 .iter()
9688 .any(|term| term.signal == "unknown_incompatible_feature")
9689 );
9690 }
9691
9692 fn add_edge_records(graph_records: &[DecisionRecord]) -> Vec<Vec<&str>> {
9698 graph_records
9699 .iter()
9700 .filter(|record| record.operation == "add_edge")
9701 .map(|record| {
9702 record
9703 .evidence
9704 .iter()
9705 .map(|term| term.signal.as_ref())
9706 .collect()
9707 })
9708 .collect()
9709 }
9710
9711 #[test]
9712 fn allowed_add_edge_records_only_the_outcome_decision() {
9713 let mut graph = Graph::strict();
9714 graph
9715 .add_edge_with_attrs("a", "b", AttrMap::new())
9716 .expect("edge add should succeed");
9717
9718 assert_eq!(
9719 add_edge_records(graph.evidence_ledger().records()),
9720 vec![vec![
9721 "self_loop",
9722 "edge_attr_count",
9723 "left_autocreated",
9724 "right_autocreated",
9725 ]]
9726 );
9727 assert_eq!(
9728 graph
9729 .evidence_ledger()
9730 .records()
9731 .iter()
9732 .filter(|record| record.operation == "add_edge")
9733 .map(|record| record.action)
9734 .collect::<Vec<_>>(),
9735 vec![DecisionAction::Allow]
9736 );
9737 }
9738
9739 #[test]
9740 fn add_edge_autocreation_files_no_add_node_record() {
9741 let mut graph = Graph::strict();
9747 graph
9748 .add_edge_with_attrs("a", "b", AttrMap::new())
9749 .expect("edge add should succeed");
9750
9751 let records = graph.evidence_ledger().records();
9752 assert_eq!(
9753 records.len(),
9754 1,
9755 "add_edge creating two nodes must file one record, got {:?}",
9756 records
9757 .iter()
9758 .map(|record| record.operation.as_ref())
9759 .collect::<Vec<_>>()
9760 );
9761 let record = &records[0];
9762 assert_eq!(record.operation, "add_edge");
9763 assert_eq!(
9764 record
9765 .evidence
9766 .iter()
9767 .filter(
9768 |term| term.signal == "left_autocreated" || term.signal == "right_autocreated"
9769 )
9770 .map(|term| term.observed_value.as_ref())
9771 .collect::<Vec<_>>(),
9772 vec!["true", "true"],
9773 "the autocreation event must still be auditable from the outcome record"
9774 );
9775
9776 let before = graph.evidence_ledger().records().len();
9778 graph.add_node("c");
9779 let records = graph.evidence_ledger().records();
9780 assert_eq!(records.len(), before + 1);
9781 assert_eq!(
9782 records.last().map(|record| record.operation.as_ref()),
9783 Some("add_node")
9784 );
9785 }
9786
9787 #[test]
9794 fn multigraph_slot_resolution_preserves_key_assignment() {
9795 let mut graph = MultiGraph::strict();
9796
9797 assert_eq!(
9799 graph
9800 .add_edge_with_attrs("a", "b", AttrMap::new())
9801 .expect("edge is allowed"),
9802 0
9803 );
9804 assert_eq!(
9805 graph
9806 .add_edge_with_attrs("a", "b", AttrMap::new())
9807 .expect("edge is allowed"),
9808 1
9809 );
9810 assert_eq!(
9811 graph
9812 .add_edge_with_attrs("b", "a", AttrMap::new())
9813 .expect("reversed endpoints are the same undirected pair"),
9814 2
9815 );
9816
9817 assert_eq!(
9819 graph
9820 .add_edge_with_attrs("a", "a", AttrMap::new())
9821 .expect("self-loop is allowed"),
9822 0
9823 );
9824 assert_eq!(
9825 graph
9826 .add_edge_with_attrs("a", "a", AttrMap::new())
9827 .expect("parallel self-loop is allowed"),
9828 1
9829 );
9830
9831 assert_eq!(
9833 graph
9834 .add_edge_with_key_and_attrs("a", "b", 9, AttrMap::new())
9835 .expect("explicit key is allowed"),
9836 9
9837 );
9838 assert_eq!(
9839 graph
9840 .add_edge_with_key_and_attrs("a", "a", 9, AttrMap::new())
9841 .expect("explicit key is allowed"),
9842 9
9843 );
9844
9845 assert_eq!(graph.node_count(), 2);
9846 assert_eq!(graph.edge_count(), 7);
9847 }
9848
9849 #[test]
9856 fn add_edge_fresh_bucket_take_matches_merge_semantics() {
9857 fn weight(value: i64) -> AttrMap {
9858 let mut attrs = AttrMap::new();
9859 attrs.insert("w".to_owned(), CgseValue::Int(value));
9860 attrs
9861 }
9862 fn stored(graph: &Graph, left: &str, right: &str) -> Vec<(String, CgseValue)> {
9863 graph
9864 .edge_attrs(left, right)
9865 .expect("edge exists")
9866 .iter()
9867 .map(|(key, value)| (key.clone(), value.clone()))
9868 .collect()
9869 }
9870
9871 let mut graph = Graph::strict();
9873 graph
9874 .add_edge_with_attrs("a", "b", weight(1))
9875 .expect("edge is allowed");
9876 assert_eq!(
9877 stored(&graph, "a", "b"),
9878 vec![("w".to_owned(), CgseValue::Int(1))]
9879 );
9880
9881 graph
9883 .add_edge_with_attrs("c", "d", AttrMap::new())
9884 .expect("edge is allowed");
9885 assert!(stored(&graph, "c", "d").is_empty());
9886
9887 graph
9890 .add_edge_with_attrs("c", "d", weight(7))
9891 .expect("edge is allowed");
9892 assert_eq!(
9893 stored(&graph, "c", "d"),
9894 vec![("w".to_owned(), CgseValue::Int(7))]
9895 );
9896
9897 graph
9900 .add_edge_with_attrs("a", "b", weight(9))
9901 .expect("edge is allowed");
9902 let mut extra = AttrMap::new();
9903 extra.insert("k".to_owned(), CgseValue::Int(2));
9904 graph
9905 .add_edge_with_attrs("a", "b", extra)
9906 .expect("edge is allowed");
9907 assert_eq!(
9908 stored(&graph, "a", "b"),
9909 vec![
9910 ("k".to_owned(), CgseValue::Int(2)),
9911 ("w".to_owned(), CgseValue::Int(9)),
9912 ]
9913 );
9914 }
9915
9916 #[test]
9917 fn add_edge_entry_probe_keeps_existing_empty_edge_structurally_unchanged() {
9918 let mut graph = Graph::strict();
9919 graph
9920 .add_edge_with_attrs("a", "b", AttrMap::new())
9921 .expect("initial edge is allowed");
9922 let revision_before = graph.revision;
9923
9924 graph
9925 .add_edge_with_attrs("b", "a", AttrMap::new())
9926 .expect("duplicate edge is allowed");
9927
9928 assert_eq!(graph.edge_count(), 1);
9929 assert_eq!(graph.neighbors("a"), Some(vec!["b"]));
9930 assert_eq!(graph.neighbors("b"), Some(vec!["a"]));
9931 assert_eq!(graph.revision, revision_before);
9932 }
9933
9934 #[test]
9946 fn add_edge_endpoint_resolution_preserves_autocreation_and_order() {
9947 fn autocreated(graph: &Graph) -> (String, String) {
9948 let record = graph
9949 .evidence_ledger()
9950 .records()
9951 .last()
9952 .expect("add_edge files an outcome record");
9953 let read = |signal: &str| {
9954 record
9955 .evidence
9956 .iter()
9957 .find(|term| term.signal == signal)
9958 .map(|term| term.observed_value.to_string())
9959 .unwrap_or_else(|| panic!("{signal} must be present"))
9960 };
9961 (read("left_autocreated"), read("right_autocreated"))
9962 }
9963
9964 let mut graph = Graph::strict();
9966 graph
9967 .add_edge_with_attrs("s", "s", AttrMap::new())
9968 .expect("self-loop is allowed");
9969 assert_eq!(autocreated(&graph), ("true".to_owned(), "true".to_owned()));
9970 assert_eq!(graph.node_count(), 1);
9971
9972 graph
9974 .add_edge_with_attrs("s", "s", AttrMap::new())
9975 .expect("re-adding a self-loop is allowed");
9976 assert_eq!(
9977 autocreated(&graph),
9978 ("false".to_owned(), "false".to_owned())
9979 );
9980 assert_eq!(graph.node_count(), 1);
9981
9982 graph
9984 .add_edge_with_attrs("s", "t", AttrMap::new())
9985 .expect("edge is allowed");
9986 assert_eq!(autocreated(&graph), ("false".to_owned(), "true".to_owned()));
9987
9988 graph
9991 .add_edge_with_attrs("z", "a", AttrMap::new())
9992 .expect("edge is allowed");
9993 assert_eq!(autocreated(&graph), ("true".to_owned(), "true".to_owned()));
9994 assert_eq!(graph.get_node_index("s"), Some(0));
9995 assert_eq!(graph.get_node_index("t"), Some(1));
9996 assert_eq!(graph.get_node_index("z"), Some(2));
9997 assert_eq!(graph.get_node_index("a"), Some(3));
9998 }
9999
10000 #[test]
10007 fn mutation_ledger_constants_are_borrowed_not_allocated() {
10008 use std::borrow::Cow;
10009 use std::collections::BTreeMap;
10010
10011 let mut graph = Graph::strict();
10012 let mut attrs = AttrMap::new();
10013 attrs.insert("weight".to_owned(), CgseValue::Int(3));
10014 graph
10015 .add_edge_with_attrs("a", "b", attrs)
10016 .expect("strict add_edge with an int attribute is allowed");
10017 graph.add_node("c");
10018
10019 let records = graph.evidence_ledger().records();
10020 assert_eq!(
10021 records
10022 .iter()
10023 .map(|record| record.operation.as_ref())
10024 .collect::<Vec<_>>(),
10025 vec!["add_edge", "add_node"],
10026 "ledger shape must be unchanged by the Cow migration"
10027 );
10028
10029 let expected_borrowed: BTreeMap<&str, &str> = [
10032 ("self_loop", "false"),
10033 ("left_autocreated", "true"),
10034 ("right_autocreated", "true"),
10035 ("node_preexisting", "false"),
10036 ]
10037 .into_iter()
10038 .collect();
10039
10040 for record in records {
10041 assert!(
10042 matches!(record.operation, Cow::Borrowed(_)),
10043 "operation `{}` must be borrowed",
10044 record.operation
10045 );
10046 assert!(
10047 matches!(record.rationale, Cow::Borrowed(_)),
10048 "rationale `{}` must be borrowed",
10049 record.rationale
10050 );
10051 for term in &record.evidence {
10052 assert!(
10053 matches!(term.signal, Cow::Borrowed(_)),
10054 "signal `{}` must be borrowed",
10055 term.signal
10056 );
10057 if let Some(expected) = expected_borrowed.get(term.signal.as_ref()) {
10058 assert_eq!(&term.observed_value, expected);
10059 assert!(
10060 matches!(term.observed_value, Cow::Borrowed(_)),
10061 "boolean observed_value for `{}` must be borrowed",
10062 term.signal
10063 );
10064 }
10065 }
10066 }
10067
10068 let edge_record = &records[0];
10070 assert_eq!(
10071 edge_record
10072 .evidence
10073 .iter()
10074 .find(|term| term.signal == "edge_attr_count")
10075 .map(|term| term.observed_value.as_ref()),
10076 Some("1")
10077 );
10078 }
10079
10080 #[test]
10081 fn allowed_multigraph_add_edge_records_only_the_outcome_decision() {
10082 let mut graph = MultiGraph::strict();
10083 let _ = graph
10084 .add_edge_with_attrs("a", "b", AttrMap::new())
10085 .expect("edge add should succeed");
10086
10087 assert_eq!(
10088 add_edge_records(graph.evidence_ledger().records()),
10089 vec![vec![
10090 "edge_key",
10091 "edge_attr_count",
10092 "left_autocreated",
10093 "right_autocreated",
10094 ]]
10095 );
10096 }
10097
10098 #[test]
10099 fn refused_multigraph_add_edge_still_records_its_gate_decision() {
10100 let mut graph = MultiGraph::strict();
10104 let mut attrs = AttrMap::new();
10105 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
10106 let err = graph
10107 .add_edge_with_attrs("a", "b", attrs)
10108 .expect_err("strict mode should fail closed");
10109 assert_eq!(
10110 err,
10111 GraphError::FailClosed {
10112 operation: "add_edge",
10113 reason: "incompatible edge metadata".to_owned(),
10114 }
10115 );
10116
10117 assert_eq!(
10118 add_edge_records(graph.evidence_ledger().records()),
10119 vec![vec!["unknown_incompatible_feature"]]
10120 );
10121 assert_eq!(
10122 graph
10123 .evidence_ledger()
10124 .records()
10125 .last()
10126 .map(|record| record.action),
10127 Some(DecisionAction::FailClosed)
10128 );
10129 }
10130
10131 #[test]
10132 fn revision_increments_on_mutating_operations() {
10133 let mut graph = Graph::strict();
10134 let r0 = graph.revision();
10135 let _ = graph.add_node("a");
10136 let r1 = graph.revision();
10137 assert!(r1 > r0);
10138
10139 graph.add_edge("a", "b").expect("edge add should succeed");
10140 let r2 = graph.revision();
10141 assert!(r2 > r1);
10142
10143 let _ = graph.remove_edge("a", "b");
10144 let r3 = graph.revision();
10145 assert!(r3 > r2);
10146 }
10147
10148 #[test]
10149 fn add_node_with_attrs_reports_change_on_existing_node() {
10150 let mut graph = Graph::strict();
10151 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
10152 let r1 = graph.revision();
10153 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
10154 let r2 = graph.revision();
10155 assert!(r2 > r1);
10156 let expected = CgseValue::from("blue");
10157 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
10158 }
10159
10160 #[test]
10161 fn add_node_with_identical_attrs_keeps_order_and_revision() {
10162 let mut graph = Graph::strict();
10163 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
10164 assert!(graph.add_node("b"));
10165 let revision_before = graph.revision();
10166
10167 assert!(!graph.add_node_with_attrs("a", single_attr("color", "red")));
10168
10169 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
10170 assert_eq!(graph.revision(), revision_before);
10171 assert_eq!(
10172 graph.node_attrs("a").and_then(|attrs| attrs.get("color")),
10173 Some(&CgseValue::from("red"))
10174 );
10175 }
10176
10177 #[test]
10178 fn add_node_with_attrs_keeps_fresh_node_order_and_attrs() {
10179 let mut graph = Graph::strict();
10180 assert!(graph.add_node("before"));
10181 assert!(graph.add_node_with_attrs("fresh", single_attr("color", "red")));
10182 assert!(graph.add_node("after"));
10183
10184 assert_eq!(graph.nodes_ordered(), vec!["before", "fresh", "after"]);
10185 assert_eq!(
10186 graph
10187 .node_attrs("fresh")
10188 .and_then(|attrs| attrs.get("color")),
10189 Some(&CgseValue::from("red"))
10190 );
10191 }
10192
10193 #[test]
10194 fn multigraph_add_node_with_attrs_reports_change_on_existing_node() {
10195 let mut graph = MultiGraph::strict();
10196 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
10197 let r1 = graph.revision();
10198 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
10199 let r2 = graph.revision();
10200 assert!(r2 > r1);
10201 let expected = CgseValue::from("blue");
10202 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
10203 }
10204
10205 #[test]
10206 fn hardened_self_loop_records_allow_decision() {
10207 let mut graph = Graph::hardened();
10208 graph
10209 .add_edge("loop", "loop")
10210 .expect("hardened self-loop edge should be accepted");
10211
10212 let add_edge_record = graph
10213 .evidence_ledger()
10214 .records()
10215 .iter()
10216 .rev()
10217 .find(|record| record.operation == "add_edge")
10218 .expect("add_edge operation should emit ledger row");
10219 assert_decision_record_schema(add_edge_record, CompatibilityMode::Hardened);
10220 assert_eq!(add_edge_record.action, DecisionAction::Allow);
10221 assert!(
10222 add_edge_record
10223 .evidence
10224 .iter()
10225 .any(|term| term.signal == "self_loop" && term.observed_value == "true")
10226 );
10227 }
10228
10229 #[test]
10230 fn runtime_policy_tracks_hardened_graph_recovery() {
10231 let mut graph = Graph::hardened();
10232 graph
10233 .add_edge("a", "b")
10234 .expect("hardened graph edge add should succeed");
10235
10236 assert_eq!(graph.runtime_policy().mode(), CompatibilityMode::Hardened);
10237 assert!(!graph.runtime_policy().decision_log().records().is_empty());
10238 assert!(graph.runtime_policy().posterior().observation_count >= 1);
10239 }
10240
10241 #[test]
10242 fn snapshot_roundtrip_replays_to_identical_state() {
10243 let mut graph = Graph::strict();
10244
10245 let mut first_attrs = AttrMap::new();
10246 first_attrs.insert("weight".to_owned(), "7".into());
10247 graph
10248 .add_edge_with_attrs("a", "b", first_attrs)
10249 .expect("edge insert should succeed");
10250
10251 let mut second_attrs = AttrMap::new();
10252 second_attrs.insert("color".to_owned(), "green".into());
10253 graph
10254 .add_edge_with_attrs("b", "c", second_attrs)
10255 .expect("edge insert should succeed");
10256
10257 let snapshot = graph.snapshot();
10258 let mut replayed = Graph::new(snapshot.mode);
10259 for node in &snapshot.nodes {
10260 let _ = replayed.add_node(node.clone());
10261 }
10262 for edge in &snapshot.edges {
10263 replayed
10264 .add_edge_with_attrs(edge.left.clone(), edge.right.clone(), edge.attrs.clone())
10265 .expect("snapshot replay should be valid");
10266 }
10267
10268 assert_eq!(replayed.snapshot(), snapshot);
10269 assert_graph_core_invariants(&replayed);
10270 }
10271
10272 #[test]
10273 fn multigraph_tracks_parallel_edges_with_distinct_keys() {
10274 let mut graph = MultiGraph::strict();
10275 let first = graph.add_edge("a", "b").expect("edge add should succeed");
10276 let second = graph.add_edge("a", "b").expect("edge add should succeed");
10277
10278 assert_ne!(first, second);
10279 assert_eq!(graph.edge_count(), 2);
10280 assert_eq!(graph.edge_keys("a", "b"), Some(vec![0, 1]));
10281 assert_multigraph_core_invariants(&graph);
10282 }
10283
10284 #[test]
10285 fn multigraph_counts_parallel_selfloops_only() {
10286 let mut graph = MultiGraph::strict();
10287 let _ = graph
10288 .add_edge("a", "a")
10289 .expect("self-loop add should succeed");
10290 let _ = graph
10291 .add_edge("a", "a")
10292 .expect("self-loop add should succeed");
10293 let _ = graph
10294 .add_edge("b", "b")
10295 .expect("self-loop add should succeed");
10296 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
10297 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
10298
10299 assert_eq!(graph.number_of_selfloops(), 3);
10300 assert_multigraph_core_invariants(&graph);
10301 }
10302
10303 #[test]
10304 fn multigraph_add_fresh_edge_unrecorded_rejects_occupied_bucket() {
10305 let mut graph = MultiGraph::strict();
10306 assert_eq!(graph.add_fresh_edge_unrecorded("a", "b"), Some(0));
10307 let revision = graph.revision();
10308 let edge_count = graph.edge_count();
10309
10310 assert_eq!(graph.add_fresh_edge_unrecorded("b", "a"), None);
10311
10312 assert_eq!(graph.revision(), revision);
10313 assert_eq!(graph.edge_count(), edge_count);
10314 assert_eq!(graph.edge_keys("a", "b"), Some(vec![0]));
10315 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
10316 assert_multigraph_core_invariants(&graph);
10317 }
10318
10319 #[test]
10320 fn multigraph_add_fresh_edge_with_key_unrecorded_preserves_key() {
10321 let mut graph = MultiGraph::strict();
10322
10323 assert_eq!(
10324 graph.add_fresh_edge_with_key_unrecorded("a", "b", 42),
10325 Some(42)
10326 );
10327
10328 assert_eq!(graph.edge_count(), 1);
10329 assert_eq!(graph.edge_keys("a", "b"), Some(vec![42]));
10330 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
10331 assert_multigraph_core_invariants(&graph);
10332 }
10333
10334 #[test]
10335 fn multigraph_remove_edge_without_key_removes_latest_instance() {
10336 let mut graph = MultiGraph::strict();
10337 let first = graph.add_edge("a", "b").expect("edge add should succeed");
10338 let second = graph.add_edge("a", "b").expect("edge add should succeed");
10339
10340 assert!(graph.remove_edge("a", "b", None));
10341 assert_eq!(graph.edge_count(), 1);
10342 assert_eq!(graph.edge_keys("a", "b"), Some(vec![first]));
10343 assert_ne!(first, second);
10344 assert_multigraph_core_invariants(&graph);
10345 }
10346
10347 #[test]
10348 fn multigraph_remove_node_clears_parallel_incident_edges() {
10349 let mut graph = MultiGraph::strict();
10350 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
10351 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
10352 let _ = graph.add_edge("b", "c").expect("edge add should succeed");
10353
10354 assert!(graph.remove_node("b"));
10355 assert_eq!(graph.edge_count(), 0);
10356 assert!(!graph.has_node("b"));
10357 assert_eq!(graph.neighbors("a"), Some(vec![]));
10358 assert_eq!(graph.neighbors("c"), Some(vec![]));
10359 assert_multigraph_core_invariants(&graph);
10360 }
10361
10362 #[test]
10363 fn multigraph_clear_edges_preserves_nodes_attrs_and_rows() {
10364 let mut graph = MultiGraph::strict();
10365 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
10366 assert!(graph.add_node_with_attrs("b", single_attr("color", "blue")));
10367 let _ = graph
10368 .add_edge_with_key_and_attrs("a", "b", 7, single_attr("weight", "3"))
10369 .expect("edge add should succeed");
10370 let _ = graph
10371 .add_edge_with_key_and_attrs("b", "a", 2, single_attr("weight", "5"))
10372 .expect("edge add should succeed");
10373 let before_revision = graph.revision();
10374
10375 graph.clear_edges();
10376
10377 assert_eq!(graph.node_count(), 2);
10378 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
10379 assert_eq!(graph.edge_count(), 0);
10380 assert!(graph.edges_ordered().is_empty());
10381 assert_eq!(graph.neighbors("a"), Some(vec![]));
10382 assert_eq!(graph.neighbors("b"), Some(vec![]));
10383 assert_eq!(
10384 graph.node_attrs("a").unwrap().get("color"),
10385 Some(&CgseValue::from("red"))
10386 );
10387 assert!(graph.revision() > before_revision);
10388 assert_multigraph_core_invariants(&graph);
10389 }
10390
10391 #[test]
10392 fn runtime_policy_tracks_hardened_multigraph_recovery() {
10393 let mut graph = MultiGraph::hardened();
10394 graph
10395 .add_edge("a", "b")
10396 .expect("hardened multigraph edge add should succeed");
10397
10398 assert_eq!(graph.runtime_policy().mode(), CompatibilityMode::Hardened);
10399 assert!(!graph.runtime_policy().decision_log().records().is_empty());
10400 assert!(graph.runtime_policy().posterior().observation_count >= 1);
10401 }
10402
10403 #[test]
10404 fn multigraph_roundtrips_sparse_snapshot_keys() {
10405 let mut graph = MultiGraph::strict();
10406 assert_eq!(
10407 graph.add_edge("a", "b").expect("edge add should succeed"),
10408 0
10409 );
10410 assert_eq!(
10411 graph.add_edge("a", "b").expect("edge add should succeed"),
10412 1
10413 );
10414 assert_eq!(
10415 graph.add_edge("a", "b").expect("edge add should succeed"),
10416 2
10417 );
10418 assert!(graph.remove_edge("a", "b", Some(1)));
10419 assert_eq!(
10420 graph.add_edge("a", "b").expect("edge add should succeed"),
10421 3
10422 );
10423
10424 let snapshot = graph.snapshot();
10425 assert_eq!(
10426 snapshot
10427 .edges
10428 .iter()
10429 .map(|edge| edge.key)
10430 .collect::<Vec<_>>(),
10431 vec![0, 2, 3]
10432 );
10433
10434 let mut replayed = MultiGraph::new(snapshot.mode);
10435 for node in &snapshot.nodes {
10436 let _ = replayed.add_node(node.clone());
10437 }
10438 for edge in &snapshot.edges {
10439 replayed
10440 .add_edge_with_key_and_attrs(
10441 edge.left.clone(),
10442 edge.right.clone(),
10443 edge.key,
10444 edge.attrs.clone(),
10445 )
10446 .expect("snapshot replay should preserve explicit keys");
10447 }
10448
10449 assert_eq!(replayed.snapshot(), snapshot);
10450 assert_multigraph_core_invariants(&replayed);
10451 }
10452
10453 proptest! {
10454 #[test]
10455 fn prop_core_invariants_hold_for_mixed_edge_mutations(
10456 ops in prop::collection::vec((0_u8..8, 0_u8..8, any::<bool>()), 1..80),
10457 ) {
10458 let mut graph = Graph::strict();
10459 let mut last_revision = graph.revision();
10460
10461 for (left_id, right_id, is_add) in ops {
10462 let left = node_name(left_id);
10463 let right = node_name(right_id);
10464 if is_add {
10465 prop_assert!(graph.add_edge(left.clone(), right.clone()).is_ok());
10466 } else {
10467 let _ = graph.remove_edge(&left, &right);
10468 }
10469 let revision = graph.revision();
10470 prop_assert!(revision >= last_revision);
10471 last_revision = revision;
10472 assert_graph_core_invariants(&graph);
10473 }
10474 }
10475
10476 #[test]
10477 fn prop_snapshot_is_deterministic_for_same_operation_stream(
10478 ops in prop::collection::vec((0_u8..8, 0_u8..8, 0_u8..3), 0..64),
10479 ) {
10480 let mut graph_left = Graph::hardened();
10481 let mut graph_right = Graph::hardened();
10482
10483 for (left_id, right_id, attrs_variant) in ops {
10484 let left = node_name(left_id);
10485 let right = node_name(right_id);
10486 let mut attrs = AttrMap::new();
10487 if attrs_variant == 1 {
10488 attrs.insert("weight".to_owned(), (left_id % 5).to_string().into());
10489 } else if attrs_variant == 2 {
10490 attrs.insert("tag".to_owned(), format!("k{}", right_id % 4).into());
10491 }
10492 prop_assert!(
10493 graph_left
10494 .add_edge_with_attrs(left.clone(), right.clone(), attrs.clone())
10495 .is_ok()
10496 );
10497 prop_assert!(
10498 graph_right
10499 .add_edge_with_attrs(left, right, attrs)
10500 .is_ok()
10501 );
10502 }
10503
10504 prop_assert_eq!(graph_left.snapshot(), graph_right.snapshot());
10505 prop_assert_eq!(graph_left.snapshot(), graph_left.snapshot());
10506 }
10507
10508 #[test]
10509 fn prop_reapplying_identical_edge_attrs_is_revision_stable(
10510 left_id in 0_u8..8,
10511 right_id in 0_u8..8,
10512 weight in 0_u16..5000,
10513 ) {
10514 let mut graph = Graph::strict();
10515 let left = node_name(left_id);
10516 let right = node_name(right_id);
10517 let mut attrs = AttrMap::new();
10518 attrs.insert("weight".to_owned(), weight.to_string().into());
10519
10520 prop_assert!(
10521 graph
10522 .add_edge_with_attrs(left.clone(), right.clone(), attrs.clone())
10523 .is_ok()
10524 );
10525 let revision_after_first = graph.revision();
10526 prop_assert!(
10527 graph
10528 .add_edge_with_attrs(left, right, attrs)
10529 .is_ok()
10530 );
10531 prop_assert_eq!(graph.revision(), revision_after_first);
10532 }
10533
10534 #[test]
10535 fn prop_remove_node_clears_incident_edges(
10536 ops in prop::collection::vec((0_u8..8, 0_u8..8), 1..64),
10537 target_id in 0_u8..8,
10538 ) {
10539 let mut graph = Graph::strict();
10540 for (left_id, right_id) in ops {
10541 let left = node_name(left_id);
10542 let right = node_name(right_id);
10543 prop_assert!(graph.add_edge(left, right).is_ok());
10544 }
10545
10546 let target = node_name(target_id);
10547 let removed = graph.remove_node(&target);
10548 if removed {
10549 prop_assert!(!graph.has_node(&target));
10550 for node in graph.nodes_ordered() {
10551 let neighbors = graph
10552 .neighbors(node)
10553 .expect("graph nodes should always have adjacency buckets");
10554 prop_assert!(!neighbors.contains(&target.as_str()));
10555 prop_assert!(!graph.has_edge(node, &target));
10556 }
10557 }
10558 assert_graph_core_invariants(&graph);
10559 }
10560
10561 #[test]
10562 fn prop_decision_ledger_records_follow_schema(
10563 ops in prop::collection::vec((0_u8..8, 0_u8..8, 0_u8..4), 1..72),
10564 ) {
10565 let mut graph = Graph::strict();
10566 for (left_id, right_id, attrs_kind) in ops {
10567 let left = node_name(left_id);
10568 let right = node_name(right_id);
10569 let mut attrs = AttrMap::new();
10570 match attrs_kind {
10571 0 => {}
10572 1 => {
10573 attrs.insert("weight".to_owned(), (left_id % 9).to_string().into());
10574 }
10575 2 => {
10576 attrs.insert("color".to_owned(), format!("c{}", right_id % 6).into());
10577 }
10578 _ => {
10579 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
10580 }
10581 }
10582 let _ = graph.add_edge_with_attrs(left, right, attrs);
10583 }
10584
10585 let records = graph.evidence_ledger().records();
10586 prop_assert!(!records.is_empty());
10587 for record in records {
10588 assert_decision_record_schema(record, CompatibilityMode::Strict);
10589 if record.operation == "add_node" {
10590 prop_assert_eq!(record.action, DecisionAction::Allow);
10591 prop_assert!(record.evidence.iter().any(|term| term.signal == "node_preexisting"));
10592 prop_assert!(record.evidence.iter().any(|term| term.signal == "attrs_count"));
10593 } else {
10594 prop_assert_eq!(&record.operation, "add_edge");
10595 if record.action == DecisionAction::FailClosed {
10596 prop_assert!(
10597 record
10598 .evidence
10599 .iter()
10600 .any(|term| term.signal == "unknown_incompatible_feature")
10601 );
10602 } else {
10603 prop_assert_eq!(record.action, DecisionAction::Allow);
10604 let is_precheck = record
10608 .evidence
10609 .iter()
10610 .any(|term| term.signal == "unknown_incompatible_feature")
10611 && !record
10612 .evidence
10613 .iter()
10614 .any(|term| term.signal == "edge_attr_count");
10615 if !is_precheck {
10616 prop_assert!(record.evidence.iter().any(|term| term.signal == "edge_attr_count"));
10617 prop_assert!(record.evidence.iter().any(|term| term.signal == "self_loop"));
10618 }
10619 }
10620 }
10621 }
10622 }
10623 }
10624}
10625
10626#[test]
10650#[ignore = "measurement; run with --release --ignored --nocapture"]
10651fn node_decision_ledger_self_time_ab() {
10652 use std::hint::black_box;
10653 use std::time::{Duration, Instant};
10654
10655 const NODES: usize = 2000;
10656 const REPS: usize = 200;
10657 const ROUNDS: usize = 41;
10658
10659 let keys: Vec<String> = (0..NODES).map(|n| format!("n{n}")).collect();
10660 let mut graph = Graph::strict();
10661 for key in &keys {
10662 let _ = graph.add_node_with_attrs(key.clone(), AttrMap::new());
10663 }
10664 let before = graph.node_count();
10665
10666 let recorded = |g: &mut Graph| -> Duration {
10669 let start = Instant::now();
10670 for _ in 0..REPS {
10671 for key in &keys {
10672 black_box(g.add_node_with_attrs(key.as_str(), AttrMap::new()));
10673 }
10674 }
10675 start.elapsed()
10676 };
10677 let unrecorded = |g: &mut Graph| -> Duration {
10678 let start = Instant::now();
10679 for _ in 0..REPS {
10680 for key in &keys {
10681 black_box(g.add_node_with_attrs_unrecorded(key.as_str(), AttrMap::new()));
10682 }
10683 }
10684 start.elapsed()
10685 };
10686
10687 let balanced = |first: &dyn Fn(&mut Graph) -> Duration,
10690 second: &dyn Fn(&mut Graph) -> Duration,
10691 g: &mut Graph|
10692 -> (f64, f64) {
10693 let a1 = first(g);
10694 let b1 = second(g);
10695 let b2 = second(g);
10696 let a2 = first(g);
10697 let scale = (2 * NODES * REPS) as f64;
10698 (
10699 (a1 + a2).as_nanos() as f64 / scale,
10700 (b1 + b2).as_nanos() as f64 / scale,
10701 )
10702 };
10703
10704 for _ in 0..3 {
10705 recorded(&mut graph);
10706 unrecorded(&mut graph);
10707 }
10708
10709 let mut rec_ns = Vec::with_capacity(ROUNDS);
10710 let mut unrec_ns = Vec::with_capacity(ROUNDS);
10711 let mut null_a = Vec::with_capacity(ROUNDS);
10712 let mut null_b = Vec::with_capacity(ROUNDS);
10713 for _ in 0..ROUNDS {
10714 let (na, nb) = balanced(&recorded, &recorded, &mut graph);
10715 null_a.push(na);
10716 null_b.push(nb);
10717 let (r, u) = balanced(&recorded, &unrecorded, &mut graph);
10718 rec_ns.push(r);
10719 unrec_ns.push(u);
10720 }
10721
10722 assert_eq!(
10723 graph.node_count(),
10724 before,
10725 "a no-op control that changed the node count measured a real insert"
10726 );
10727
10728 let median = |v: &mut Vec<f64>| -> f64 {
10729 v.sort_by(f64::total_cmp);
10730 v[(v.len() - 1) / 2]
10731 };
10732 let rec = median(&mut rec_ns);
10733 let unrec = median(&mut unrec_ns);
10734 let na = median(&mut null_a);
10735 let nb = median(&mut null_b);
10736
10737 println!("node_decision_ledger_self_time_ab nodes={NODES} reps={REPS} rounds={ROUNDS}");
10738 println!(" A/A null (recorded/recorded) : {:8.4}x", nb / na);
10739 println!(" A add_node_with_attrs : {rec:8.1} ns/call (store + ledger)");
10740 println!(" B ..._unrecorded : {unrec:8.1} ns/call (store only)");
10741 println!(
10742 " -> decision ledger costs : {:8.1} ns/call",
10743 rec - unrec
10744 );
10745 println!(
10746 " -> ledger share of the pair : {:8.1}%",
10747 100.0 * (rec - unrec) / rec
10748 );
10749}