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::IndexMap;
10use serde::{Deserialize, Serialize};
11use std::collections::BTreeMap;
12
13pub(crate) type FxIndexMap<K, V> = IndexMap<K, V, rustc_hash::FxBuildHasher>;
20pub(crate) type FxIndexSet<T> = indexmap::IndexSet<T, rustc_hash::FxBuildHasher>;
26use std::collections::HashSet;
27use std::fmt;
28
29pub use fnx_runtime::AttrMap;
30
31#[derive(Debug, Clone, PartialEq, Eq)]
32pub enum GraphError {
33 FailClosed {
34 operation: &'static str,
35 reason: String,
36 },
37}
38
39impl fmt::Display for GraphError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::FailClosed { operation, reason } => {
43 write!(f, "operation `{operation}` failed closed: {reason}")
44 }
45 }
46 }
47}
48
49impl std::error::Error for GraphError {}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct EdgeSnapshot {
53 pub left: String,
54 pub right: String,
55 pub attrs: AttrMap,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59pub struct GraphSnapshot {
60 pub mode: CompatibilityMode,
61 pub nodes: Vec<String>,
62 #[serde(default)]
68 pub node_attrs: BTreeMap<String, AttrMap>,
69 pub edges: Vec<EdgeSnapshot>,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub struct MultiEdgeSnapshot {
74 pub left: String,
75 pub right: String,
76 pub key: usize,
77 pub attrs: AttrMap,
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
81pub struct MultiGraphSnapshot {
82 pub mode: CompatibilityMode,
83 pub nodes: Vec<String>,
84 #[serde(default)]
86 pub node_attrs: BTreeMap<String, AttrMap>,
87 pub edges: Vec<MultiEdgeSnapshot>,
88}
89
90#[derive(Debug, Clone, Default)]
101pub(crate) struct NodeOrder {
102 slots: rustc_hash::FxHashMap<String, usize>,
103 pub(crate) names: Vec<Option<String>>,
106 live: usize,
107 ranks: std::sync::OnceLock<NodeRanks>,
108 names_are_positions: std::sync::OnceLock<bool>,
112}
113
114fn is_decimal_of(name: &str, mut n: usize) -> bool {
116 let digits = name.as_bytes();
117 let mut i = digits.len();
118 loop {
119 if i == 0 {
120 return false;
121 }
122 i -= 1;
123 #[allow(clippy::cast_possible_truncation)]
124 if digits[i] != b'0' + (n % 10) as u8 {
125 return false;
126 }
127 n /= 10;
128 if n == 0 {
129 return i == 0;
130 }
131 }
132}
133
134#[derive(Debug, Clone)]
138struct NodeRanks {
139 tree: Vec<usize>,
141}
142
143impl NodeRanks {
144 fn build(live: impl ExactSizeIterator<Item = bool>) -> Self {
146 let n = live.len();
147 let mut tree = vec![0; n + 1];
148 for (entry, is_live) in live.enumerate() {
149 if is_live {
150 tree[entry + 1] += 1;
151 }
152 }
153 for i in 1..=n {
154 let parent = i + i.isolate_lowest_one();
155 if parent <= n {
156 tree[parent] += tree[i];
157 }
158 }
159 Self { tree }
160 }
161
162 fn rank(&self, slot: usize) -> usize {
164 let mut i = slot;
165 let mut live = 0;
166 while i > 0 {
167 live += self.tree[i];
168 i &= i - 1;
169 }
170 live
171 }
172
173 fn select(&self, position: usize) -> Option<usize> {
175 let n = self.tree.len() - 1;
176 if n == 0 {
177 return None;
178 }
179 let mut remaining = position + 1;
180 let mut index = 0;
181 let mut step = 1 << (usize::BITS - 1 - n.leading_zeros());
182 while step > 0 {
183 let next = index + step;
184 if next <= n && self.tree[next] < remaining {
185 index = next;
186 remaining -= self.tree[next];
187 }
188 step >>= 1;
189 }
190 (index < n).then_some(index)
191 }
192
193 fn remove(&mut self, slot: usize) {
194 let mut i = slot + 1;
195 while i < self.tree.len() {
196 self.tree[i] -= 1;
197 i += i.isolate_lowest_one();
198 }
199 }
200
201 fn push(&mut self, is_live: bool) {
204 let i = self.tree.len();
205 let covered = self.rank(i - 1) - self.rank(i - i.isolate_lowest_one());
206 self.tree.push(covered + usize::from(is_live));
207 }
208
209 fn pop(&mut self) {
211 debug_assert!(self.tree.len() > 1);
212 self.tree.pop();
213 }
214}
215
216impl NodeOrder {
217 pub(crate) fn from_names(names: Vec<String>) -> Self {
219 let mut order = Self::default();
220 order.reserve(names.len());
221 for name in names {
222 order.push(name);
223 }
224 order
225 }
226
227 #[inline]
228 pub(crate) fn len(&self) -> usize {
229 self.live
230 }
231
232 #[inline]
233 pub(crate) fn is_empty(&self) -> bool {
234 self.live == 0
235 }
236
237 #[inline]
239 pub(crate) fn slot_count(&self) -> usize {
240 self.names.len()
241 }
242
243 #[inline]
245 pub(crate) fn is_dense(&self) -> bool {
246 self.live == self.names.len()
247 }
248
249 #[inline]
251 pub(crate) fn get(&self, name: &str) -> Option<&usize> {
252 self.slots.get(name)
253 }
254
255 #[inline]
256 pub(crate) fn contains_key(&self, name: &str) -> bool {
257 self.slots.contains_key(name)
258 }
259
260 #[inline]
262 pub(crate) fn name(&self, slot: usize) -> Option<&str> {
263 self.names.get(slot)?.as_deref()
264 }
265
266 pub(crate) fn reserve(&mut self, additional: usize) {
267 self.slots.reserve(additional);
268 self.names.reserve(additional);
269 }
270
271 pub(crate) fn push(&mut self, name: String) -> usize {
273 debug_assert!(!self.slots.contains_key(&name));
274 let slot = self.names.len();
275 self.slots.insert(name.clone(), slot);
276 if let Some(flag) = self.names_are_positions.get_mut()
277 && *flag
278 {
279 *flag = is_decimal_of(&name, self.live);
280 }
281 self.names.push(Some(name));
282 if let Some(ranks) = self.ranks.get_mut() {
283 ranks.push(true);
284 }
285 self.live += 1;
286 slot
287 }
288
289 pub(crate) fn remove(&mut self, name: &str) -> Option<(usize, bool)> {
294 let slot = self.slots.remove(name)?;
295 self.live -= 1;
296 let popped = slot + 1 == self.names.len() && self.live == slot;
297 if popped {
298 self.names.pop();
299 self.ranks.take();
300 } else {
301 self.names[slot] = None;
302 if let Some(ranks) = self.ranks.get_mut() {
303 ranks.remove(slot);
304 }
305 self.names_are_positions = std::sync::OnceLock::from(false);
307 }
308 Some((slot, popped))
309 }
310
311 pub(crate) fn names_are_positions(&self) -> bool {
314 *self.names_are_positions.get_or_init(|| {
315 self.keys()
316 .enumerate()
317 .all(|(position, name)| is_decimal_of(name, position))
318 })
319 }
320
321 fn ranks(&self) -> &NodeRanks {
322 self.ranks
323 .get_or_init(|| NodeRanks::build(self.names.iter().map(Option::is_some)))
324 }
325
326 #[inline]
328 pub(crate) fn slot_at(&self, position: usize) -> Option<usize> {
329 if self.is_dense() {
330 (position < self.live).then_some(position)
331 } else if position < self.live {
332 self.ranks().select(position)
333 } else {
334 None
335 }
336 }
337
338 #[inline]
340 pub(crate) fn position_of(&self, slot: usize) -> Option<usize> {
341 if self.is_dense() {
342 (slot < self.live).then_some(slot)
343 } else {
344 self.names.get(slot)?.as_ref()?;
345 Some(self.ranks().rank(slot))
346 }
347 }
348
349 pub(crate) fn slot_positions(&self) -> Option<Vec<usize>> {
352 if self.is_dense() {
353 return None;
354 }
355 let mut positions = vec![usize::MAX; self.names.len()];
356 for (position, slot) in self.values().enumerate() {
357 positions[slot] = position;
358 }
359 Some(positions)
360 }
361
362 #[inline]
364 pub(crate) fn get_index(&self, position: usize) -> Option<(&String, usize)> {
365 let slot = self.slot_at(position)?;
366 Some((self.names.get(slot)?.as_ref()?, slot))
367 }
368
369 #[inline]
371 pub(crate) fn get_index_of(&self, name: &str) -> Option<usize> {
372 self.position_of(*self.get(name)?)
373 }
374
375 pub(crate) fn iter(&self) -> impl Iterator<Item = (&String, usize)> + '_ {
377 self.names
378 .iter()
379 .enumerate()
380 .filter_map(|(slot, name)| Some((name.as_ref()?, slot)))
381 }
382
383 pub(crate) fn keys(&self) -> impl Iterator<Item = &String> + '_ {
385 self.names.iter().filter_map(Option::as_ref)
386 }
387
388 pub(crate) fn values(&self) -> impl Iterator<Item = usize> + '_ {
390 self.iter().map(|(_, slot)| slot)
391 }
392
393 pub(crate) fn compact(&mut self) -> Vec<usize> {
396 let mut old_to_new = vec![usize::MAX; self.names.len()];
397 let mut names = Vec::with_capacity(self.live);
398 for (old, name) in std::mem::take(&mut self.names).into_iter().enumerate() {
399 if let Some(name) = name {
400 let new = names.len();
401 old_to_new[old] = new;
402 *self.slots.get_mut(&name).expect("live name is indexed") = new;
403 names.push(Some(name));
404 }
405 }
406 self.names = names;
407 self.ranks.take();
408 old_to_new
409 }
410}
411
412#[derive(Debug, Clone, Default)]
424pub(crate) struct SlotOrder {
425 slots: rustc_hash::FxHashMap<String, usize>,
426 names: Vec<Option<String>>,
428 free: Vec<usize>,
430 entries: Vec<usize>,
432 entry_of: Vec<usize>,
434 ranks: std::sync::OnceLock<NodeRanks>,
435 names_are_positions: std::sync::OnceLock<bool>,
440}
441
442impl SlotOrder {
443 #[inline]
444 pub(crate) fn len(&self) -> usize {
445 self.slots.len()
446 }
447
448 #[inline]
450 pub(crate) fn slot_count(&self) -> usize {
451 self.names.len()
452 }
453
454 #[inline]
456 fn is_dense(&self) -> bool {
457 self.entries.len() == self.slots.len()
458 }
459
460 #[inline]
462 pub(crate) fn get(&self, name: &str) -> Option<&usize> {
463 self.slots.get(name)
464 }
465
466 #[inline]
467 pub(crate) fn contains_key(&self, name: &str) -> bool {
468 self.slots.contains_key(name)
469 }
470
471 #[inline]
473 pub(crate) fn name(&self, slot: usize) -> Option<&str> {
474 self.names.get(slot)?.as_deref()
475 }
476
477 #[inline]
479 pub(crate) fn names(&self) -> &[Option<String>] {
480 &self.names
481 }
482
483 #[cfg(test)]
485 pub(crate) fn free_slots(&self) -> &[usize] {
486 &self.free
487 }
488
489 pub(crate) fn reserve(&mut self, additional: usize) {
490 self.slots.reserve(additional);
491 self.names.reserve(additional);
492 self.entries.reserve(additional);
493 self.entry_of.reserve(additional);
494 }
495
496 pub(crate) fn insert(&mut self, name: String) -> (usize, bool) {
500 debug_assert!(!self.slots.contains_key(&name));
501 if let Some(flag) = self.names_are_positions.get_mut()
502 && *flag
503 {
504 *flag = is_decimal_of(&name, self.slots.len());
505 }
506 let (slot, fresh) = if let Some(slot) = self.free.pop() {
507 debug_assert!(self.names[slot].is_none());
508 self.names[slot] = Some(name.clone());
509 (slot, false)
510 } else {
511 self.names.push(Some(name.clone()));
512 self.entry_of.push(usize::MAX);
513 (self.names.len() - 1, true)
514 };
515 self.slots.insert(name, slot);
516 self.entry_of[slot] = self.entries.len();
517 self.entries.push(slot);
518 if let Some(ranks) = self.ranks.get_mut() {
519 ranks.push(true);
520 }
521 (slot, fresh)
522 }
523
524 pub(crate) fn remove(&mut self, name: &str) -> Option<usize> {
527 let slot = self.slots.remove(name)?;
528 self.names[slot] = None;
529 self.free.push(slot);
530 let entry = self.entry_of[slot];
531 if entry + 1 == self.entries.len() {
532 self.entries.pop();
533 if let Some(ranks) = self.ranks.get_mut() {
534 ranks.pop();
535 }
536 } else {
537 self.entries[entry] = usize::MAX;
538 if let Some(ranks) = self.ranks.get_mut() {
539 ranks.remove(entry);
540 }
541 self.names_are_positions = std::sync::OnceLock::from(false);
543 }
544 if self.entries.len() - self.slots.len() > self.slots.len() {
545 self.compact();
546 }
547 Some(slot)
548 }
549
550 pub(crate) fn remove_slots(&mut self, doomed: &rustc_hash::FxHashSet<usize>) {
553 for &slot in doomed {
554 let name = self.names[slot].take().expect("doomed slot is live");
555 self.slots.remove(&name);
556 self.entries[self.entry_of[slot]] = usize::MAX;
557 self.free.push(slot);
558 }
559 if !doomed.is_empty() {
560 self.names_are_positions = std::sync::OnceLock::from(false);
561 }
562 self.compact();
563 }
564
565 pub(crate) fn names_are_positions(&self) -> bool {
568 *self.names_are_positions.get_or_init(|| {
569 self.keys()
570 .enumerate()
571 .all(|(position, name)| is_decimal_of(name, position))
572 })
573 }
574
575 fn compact(&mut self) {
577 self.entries.retain(|&slot| slot != usize::MAX);
578 for (entry, &slot) in self.entries.iter().enumerate() {
579 self.entry_of[slot] = entry;
580 }
581 self.ranks.take();
582 }
583
584 fn ranks(&self) -> &NodeRanks {
585 self.ranks
586 .get_or_init(|| NodeRanks::build(self.entries.iter().map(|&slot| slot != usize::MAX)))
587 }
588
589 #[inline]
591 pub(crate) fn slot_at(&self, position: usize) -> Option<usize> {
592 if self.is_dense() {
593 self.entries.get(position).copied()
594 } else if position < self.slots.len() {
595 Some(self.entries[self.ranks().select(position)?])
596 } else {
597 None
598 }
599 }
600
601 #[inline]
603 pub(crate) fn get_index_of(&self, name: &str) -> Option<usize> {
604 let entry = self.entry_of[*self.slots.get(name)?];
605 Some(if self.is_dense() {
606 entry
607 } else {
608 self.ranks().rank(entry)
609 })
610 }
611
612 #[inline]
614 pub(crate) fn get_index(&self, position: usize) -> Option<(&String, usize)> {
615 let slot = self.slot_at(position)?;
616 Some((self.names[slot].as_ref()?, slot))
617 }
618
619 pub(crate) fn iter(&self) -> impl Iterator<Item = (&String, usize)> + '_ {
621 self.entries
622 .iter()
623 .filter(|&&slot| slot != usize::MAX)
624 .map(|&slot| (self.names[slot].as_ref().expect("live entry"), slot))
625 }
626
627 pub(crate) fn keys(&self) -> impl Iterator<Item = &String> + '_ {
629 self.iter().map(|(name, _)| name)
630 }
631
632 pub(crate) fn values(&self) -> impl Iterator<Item = usize> + '_ {
634 self.iter().map(|(_, slot)| slot)
635 }
636}
637
638#[derive(Debug, Clone, Default)]
648pub(crate) struct SlotRows {
649 rows: Vec<Vec<usize>>,
650 positions: std::sync::OnceLock<Vec<Vec<usize>>>,
651}
652
653impl std::ops::Deref for SlotRows {
654 type Target = Vec<Vec<usize>>;
655
656 fn deref(&self) -> &Vec<Vec<usize>> {
657 &self.rows
658 }
659}
660
661impl SlotRows {
662 pub(crate) fn new(rows: Vec<Vec<usize>>) -> Self {
663 Self {
664 rows,
665 positions: std::sync::OnceLock::new(),
666 }
667 }
668
669 pub(crate) fn rows_mut(&mut self) -> &mut Vec<Vec<usize>> {
671 self.positions.take();
672 &mut self.rows
673 }
674
675 pub(crate) fn positions(&self, order: &NodeOrder) -> &[Vec<usize>] {
678 self.positions.get_or_init(|| {
679 let mut slot_positions = vec![usize::MAX; self.rows.len()];
680 for (position, slot) in order.values().enumerate() {
681 slot_positions[slot] = position;
682 }
683 order
684 .values()
685 .map(|slot| {
686 self.rows[slot]
687 .iter()
688 .map(|&neighbour| slot_positions[neighbour])
689 .collect()
690 })
691 .collect()
692 })
693 }
694}
695
696pub(crate) fn set_slot_row_orders(
701 rows: &mut SlotRows,
702 slots: &[usize],
703 wanted: &[Vec<usize>],
704) -> Option<bool> {
705 if wanted.len() != slots.len() {
706 return None;
707 }
708 let mut unplaced = vec![usize::MAX; rows.len()];
710 let mut ordered: Vec<Vec<usize>> = Vec::with_capacity(slots.len());
711 for (&slot, row) in slots.iter().zip(wanted) {
712 let current = &rows[slot];
713 if row.len() != current.len() {
714 return None;
715 }
716 for &w in current {
717 unplaced[w] = slot;
718 }
719 let mut new_row = Vec::with_capacity(row.len());
720 for &position in row {
721 let w = *slots.get(position)?;
722 if unplaced[w] != slot {
723 return None;
724 }
725 unplaced[w] = usize::MAX;
726 new_row.push(w);
727 }
728 ordered.push(new_row);
729 }
730 let mut changed = false;
731 for (&slot, new_row) in slots.iter().zip(ordered) {
732 if rows[slot] != new_row {
733 rows.rows_mut()[slot] = new_row;
734 changed = true;
735 }
736 }
737 Some(changed)
738}
739
740pub(crate) fn reorder_slot_rows_like(
746 rows: &mut SlotRows,
747 source_rows: &SlotRows,
748 slots: &[usize],
749 to_source: &[usize],
750 from_source: &[usize],
751) -> bool {
752 let mut in_row = vec![usize::MAX; to_source.len()];
754 let mut ordered: Vec<usize> = Vec::new();
755 let mut changed = false;
756 for &slot in slots {
757 let source_slot = to_source[slot];
758 let row = &rows[slot];
759 if source_slot == usize::MAX || row.len() < 2 {
760 continue;
761 }
762 for &w in row {
763 in_row[w] = slot;
764 }
765 ordered.clear();
766 for &source_neighbor in &source_rows[source_slot] {
767 let w = from_source[source_neighbor];
768 if w != usize::MAX && in_row[w] == slot {
769 ordered.push(w);
770 in_row[w] = usize::MAX;
771 }
772 }
773 if ordered.len() < row.len() {
774 ordered.extend(row.iter().copied().filter(|&w| in_row[w] == slot));
775 }
776 if ordered != *row {
777 rows.rows_mut()[slot].clone_from(&ordered);
778 changed = true;
779 }
780 }
781 changed
782}
783
784#[derive(Debug)]
785pub struct Graph {
786 mode: CompatibilityMode,
787 revision: u64,
788 node_order: NodeOrder,
790 node_attrs: Vec<AttrMap>,
792 adj_indices: SlotRows,
794 all_int_cache: std::sync::Arc<std::sync::RwLock<Option<(u64, String, bool)>>>,
797 edge_slots_by_neighbor: std::sync::RwLock<Option<(u64, Vec<Vec<usize>>)>>,
803 edge_index_endpoints: Vec<(usize, usize)>,
804 edges: FxIndexMap<(usize, usize), AttrMap>,
807 runtime_policy: RuntimePolicy,
808}
809
810impl Clone for Graph {
811 fn clone(&self) -> Self {
812 Self {
813 mode: self.mode,
814 revision: self.revision,
815 node_order: self.node_order.clone(),
816 node_attrs: self.node_attrs.clone(),
817 adj_indices: self.adj_indices.clone(),
818 all_int_cache: std::sync::Arc::default(),
822 edge_slots_by_neighbor: std::sync::RwLock::default(),
823 edge_index_endpoints: self.edge_index_endpoints.clone(),
824 edges: self.edges.clone(),
825 runtime_policy: self.runtime_policy.clone(),
826 }
827 }
828}
829
830impl Graph {
831 #[must_use]
832 pub fn new(mode: CompatibilityMode) -> Self {
833 Self {
834 mode,
835 revision: 0,
836 node_order: NodeOrder::default(),
837 node_attrs: Vec::new(),
838 adj_indices: SlotRows::default(),
839 all_int_cache: std::sync::Arc::default(),
840 edge_slots_by_neighbor: std::sync::RwLock::default(),
841 edge_index_endpoints: Vec::new(),
842 edges: FxIndexMap::default(),
843 runtime_policy: RuntimePolicy::new(mode),
844 }
845 }
846
847 #[must_use]
848 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
849 let mode = runtime_policy.mode();
850 Self {
851 mode,
852 revision: 0,
853 node_order: NodeOrder::default(),
854 node_attrs: Vec::new(),
855 adj_indices: SlotRows::default(),
856 all_int_cache: std::sync::Arc::default(),
857 edge_slots_by_neighbor: std::sync::RwLock::default(),
858 edge_index_endpoints: Vec::new(),
859 edges: FxIndexMap::default(),
860 runtime_policy,
861 }
862 }
863
864 pub fn clear_edges(&mut self) {
871 if self.edges.is_empty() && self.edge_index_endpoints.is_empty() {
872 return;
873 }
874 self.edges.clear();
875 self.edge_index_endpoints.clear();
876 for row in self.adj_indices.rows_mut() {
877 row.clear();
878 }
879 self.revision = self.revision.saturating_add(1);
880 }
881
882 #[must_use]
883 pub fn strict() -> Self {
884 Self::new(CompatibilityMode::Strict)
885 }
886
887 #[must_use]
888 pub fn complete_graph(mode: CompatibilityMode, n: usize) -> Self {
889 let edge_capacity = n.saturating_mul(n.saturating_sub(1)) / 2;
890 let revision = u64::try_from(n)
891 .unwrap_or(u64::MAX)
892 .saturating_add(u64::try_from(edge_capacity).unwrap_or(u64::MAX));
893 let node_labels = (0..n).map(|node| node.to_string()).collect::<Vec<_>>();
894 let node_order = NodeOrder::from_names(node_labels.clone());
895 let node_attrs = vec![AttrMap::new(); n];
896 let mut graph = Self {
897 mode,
898 revision,
899 node_order,
900 node_attrs,
901 adj_indices: SlotRows::new(vec![Vec::with_capacity(n.saturating_sub(1)); n]),
902 all_int_cache: std::sync::Arc::default(),
903 edge_slots_by_neighbor: std::sync::RwLock::default(),
904 edge_index_endpoints: Vec::with_capacity(edge_capacity),
905 edges: FxIndexMap::with_capacity_and_hasher(edge_capacity, rustc_hash::FxBuildHasher),
906 runtime_policy: RuntimePolicy::new(mode),
907 };
908
909 let rows = graph.adj_indices.rows_mut();
910 for left_index in 0..n {
911 let _left = &node_labels[left_index];
912 for (right_index, _right) in node_labels.iter().enumerate().skip(left_index + 1) {
913 rows[left_index].push(right_index);
915 rows[right_index].push(left_index);
916 graph.edge_index_endpoints.push((left_index, right_index));
917 graph
918 .edges
919 .insert(Graph::canon_pair(left_index, right_index), AttrMap::new());
920 }
921 }
922
923 graph.record_decision(
924 "complete_graph_bulk",
925 0.0,
926 false,
927 vec![EvidenceTerm {
928 signal: "nodes".into(),
929 observed_value: count_evidence(n),
930 log_likelihood_ratio: 0.0,
931 }],
932 );
933 graph
934 }
935
936 #[must_use]
940 pub fn grid_2d(mode: CompatibilityMode, m: usize, n: usize) -> Self {
941 let node_count = m.saturating_mul(n);
942 let edge_capacity = m
943 .saturating_sub(1)
944 .saturating_mul(n)
945 .saturating_add(n.saturating_sub(1).saturating_mul(m));
946 let revision = u64::try_from(node_count)
947 .unwrap_or(u64::MAX)
948 .saturating_add(u64::try_from(edge_capacity).unwrap_or(u64::MAX));
949 let label = |i: usize, j: usize| format!("({i}, {j})");
950 let idx = |i: usize, j: usize| i * n + j;
951 let mut node_order = NodeOrder::default();
952 node_order.reserve(node_count);
953 for i in 0..m {
954 for j in 0..n {
955 node_order.push(label(i, j));
956 }
957 }
958 let mut graph = Self {
959 mode,
960 revision,
961 node_order,
962 node_attrs: vec![AttrMap::new(); node_count],
963 adj_indices: SlotRows::new(vec![Vec::with_capacity(4); node_count]),
964 all_int_cache: std::sync::Arc::default(),
965 edge_slots_by_neighbor: std::sync::RwLock::default(),
966 edge_index_endpoints: Vec::with_capacity(edge_capacity),
967 edges: FxIndexMap::with_capacity_and_hasher(edge_capacity, rustc_hash::FxBuildHasher),
968 runtime_policy: RuntimePolicy::new(mode),
969 };
970 fn add_grid_edge(graph: &mut Graph, _u: String, _v: String, u_idx: usize, v_idx: usize) {
971 let rows = graph.adj_indices.rows_mut();
973 rows[u_idx].push(v_idx);
974 rows[v_idx].push(u_idx);
975 graph.edge_index_endpoints.push((u_idx, v_idx));
976 graph
977 .edges
978 .insert(Graph::canon_pair(u_idx, v_idx), AttrMap::new());
979 }
980 for i in 1..m {
982 for j in 0..n {
983 add_grid_edge(
984 &mut graph,
985 label(i, j),
986 label(i - 1, j),
987 idx(i, j),
988 idx(i - 1, j),
989 );
990 }
991 }
992 for i in 0..m {
994 for j in 1..n {
995 add_grid_edge(
996 &mut graph,
997 label(i, j),
998 label(i, j - 1),
999 idx(i, j),
1000 idx(i, j - 1),
1001 );
1002 }
1003 }
1004 graph.record_decision(
1005 "grid_2d_bulk",
1006 0.0,
1007 false,
1008 vec![EvidenceTerm {
1009 signal: "nodes".into(),
1010 observed_value: count_evidence(node_count),
1011 log_likelihood_ratio: 0.0,
1012 }],
1013 );
1014 graph
1015 }
1016
1017 pub fn grid_nd(
1023 mode: CompatibilityMode,
1024 dimensions: &[usize],
1025 periodic: &[bool],
1026 ) -> Result<Self, GraphError> {
1027 if dimensions.len() != periodic.len() {
1028 return Err(GraphError::FailClosed {
1029 operation: "grid_nd",
1030 reason: format!(
1031 "periodic flag count {} does not match dimension count {}",
1032 periodic.len(),
1033 dimensions.len()
1034 ),
1035 });
1036 }
1037 if dimensions.is_empty() {
1038 return Ok(Self::new(mode));
1039 }
1040
1041 #[derive(Clone)]
1042 struct ProductState {
1043 node_count: usize,
1044 adjacency: Vec<Vec<usize>>,
1045 edge_set: HashSet<(usize, usize)>,
1046 }
1047
1048 impl ProductState {
1049 fn empty() -> Self {
1050 Self {
1051 node_count: 0,
1052 adjacency: Vec::new(),
1053 edge_set: HashSet::new(),
1054 }
1055 }
1056
1057 fn axis(size: usize, periodic: bool) -> Self {
1058 let mut state = Self {
1059 node_count: size,
1060 adjacency: vec![Vec::new(); size],
1061 edge_set: HashSet::new(),
1062 };
1063 if size == 0 {
1064 return state;
1065 }
1066 if periodic {
1067 if size == 1 {
1068 state.add_edge(0, 0);
1069 } else {
1070 for node in 0..(size - 1) {
1071 state.add_edge(node, node + 1);
1072 }
1073 state.add_edge(size - 1, 0);
1074 }
1075 } else {
1076 for node in 0..size.saturating_sub(1) {
1077 state.add_edge(node, node + 1);
1078 }
1079 }
1080 state
1081 }
1082
1083 fn product(axis: &Self, old: &Self) -> Result<Self, GraphError> {
1084 let Some(node_count) = axis.node_count.checked_mul(old.node_count) else {
1085 return Err(GraphError::FailClosed {
1086 operation: "grid_nd",
1087 reason: "node count overflow during cartesian product".to_owned(),
1088 });
1089 };
1090 if node_count == 0 {
1091 return Ok(Self::empty());
1092 }
1093
1094 let axis_edges = axis.edges_view();
1095 let old_edges = old.edges_view();
1096 let mut state = Self {
1097 node_count,
1098 adjacency: vec![Vec::new(); node_count],
1099 edge_set: HashSet::with_capacity(
1100 axis_edges
1101 .len()
1102 .saturating_mul(old.node_count)
1103 .saturating_add(old_edges.len().saturating_mul(axis.node_count)),
1104 ),
1105 };
1106
1107 for (axis_left, axis_right) in axis_edges {
1108 let left_base = axis_left * old.node_count;
1109 let right_base = axis_right * old.node_count;
1110 for old_index in 0..old.node_count {
1111 state.add_edge(left_base + old_index, right_base + old_index);
1112 }
1113 }
1114 for axis_index in 0..axis.node_count {
1115 let base = axis_index * old.node_count;
1116 for &(old_left, old_right) in &old_edges {
1117 state.add_edge(base + old_left, base + old_right);
1118 }
1119 }
1120 Ok(state)
1121 }
1122
1123 fn add_edge(&mut self, left: usize, right: usize) {
1124 let key = if left <= right {
1125 (left, right)
1126 } else {
1127 (right, left)
1128 };
1129 if !self.edge_set.insert(key) {
1130 return;
1131 }
1132 self.adjacency[left].push(right);
1133 if left != right {
1134 self.adjacency[right].push(left);
1135 }
1136 }
1137
1138 fn edges_view(&self) -> Vec<(usize, usize)> {
1139 let mut edges = Vec::with_capacity(self.edge_set.len());
1140 let mut seen = vec![false; self.node_count];
1141 for left in 0..self.node_count {
1142 for &right in &self.adjacency[left] {
1143 if !seen[right] {
1144 edges.push((left, right));
1145 }
1146 }
1147 seen[left] = true;
1148 }
1149 edges
1150 }
1151 }
1152
1153 fn coords_from_index(mut index: usize, sizes: &[usize]) -> Vec<usize> {
1154 let mut coords = vec![0; sizes.len()];
1155 for axis in (0..sizes.len()).rev() {
1156 let size = sizes[axis];
1157 coords[axis] = index % size;
1158 index /= size;
1159 }
1160 coords
1161 }
1162
1163 fn tuple_canonical(coords: &[usize]) -> String {
1164 if let [value] = coords {
1165 return value.to_string();
1166 }
1167 let mut s = String::with_capacity(coords.len() * 6 + 2);
1168 s.push('(');
1169 for (i, value) in coords.iter().enumerate() {
1170 if i > 0 {
1171 s.push_str(", ");
1172 }
1173 s.push_str(&value.to_string());
1174 }
1175 s.push(')');
1176 s
1177 }
1178
1179 let mut state = ProductState::axis(dimensions[0], periodic[0]);
1180 for (&size, &periodic_axis) in dimensions.iter().zip(periodic).skip(1) {
1181 let axis = ProductState::axis(size, periodic_axis);
1182 state = ProductState::product(&axis, &state)?;
1183 }
1184
1185 let output_sizes = dimensions.iter().rev().copied().collect::<Vec<_>>();
1186 let labels = (0..state.node_count)
1187 .map(|index| tuple_canonical(&coords_from_index(index, &output_sizes)))
1188 .collect::<Vec<_>>();
1189 let edge_view = state.edges_view();
1190 let mut final_adjacency = vec![Vec::new(); state.node_count];
1191 for &(left, right) in &edge_view {
1192 final_adjacency[left].push(right);
1193 if left != right {
1194 final_adjacency[right].push(left);
1195 }
1196 }
1197 let revision = u64::try_from(state.node_count)
1198 .unwrap_or(u64::MAX)
1199 .saturating_add(u64::try_from(edge_view.len()).unwrap_or(u64::MAX));
1200 let node_order = NodeOrder::from_names(labels);
1201
1202 let mut graph = Self {
1203 mode,
1204 revision,
1205 node_order,
1206 node_attrs: vec![AttrMap::new(); state.node_count],
1207 adj_indices: SlotRows::new(final_adjacency),
1208 all_int_cache: std::sync::Arc::default(),
1209 edge_slots_by_neighbor: std::sync::RwLock::default(),
1210 edge_index_endpoints: Vec::with_capacity(edge_view.len()),
1211 edges: FxIndexMap::with_capacity_and_hasher(edge_view.len(), rustc_hash::FxBuildHasher),
1212 runtime_policy: RuntimePolicy::new(mode),
1213 };
1214
1215 for (left, right) in edge_view {
1216 graph.edge_index_endpoints.push((left, right));
1217 graph
1218 .edges
1219 .insert(Graph::canon_pair(left, right), AttrMap::new());
1220 }
1221
1222 graph.record_decision(
1223 "grid_nd_bulk",
1224 0.0,
1225 false,
1226 vec![EvidenceTerm {
1227 signal: "nodes".into(),
1228 observed_value: count_evidence(state.node_count),
1229 log_likelihood_ratio: 0.0,
1230 }],
1231 );
1232 Ok(graph)
1233 }
1234
1235 #[must_use]
1248 pub fn kneser(mode: CompatibilityMode, n: usize, k: usize) -> Self {
1249 fn tuple_canonical(c: &[usize]) -> String {
1250 let mut s = String::with_capacity(c.len() * 6 + 2);
1251 s.push('(');
1252 for (i, v) in c.iter().enumerate() {
1253 if i > 0 {
1254 s.push_str(", ");
1255 }
1256 s.push_str(&v.to_string());
1257 }
1258 if c.len() == 1 {
1259 s.push(',');
1260 }
1261 s.push(')');
1262 s
1263 }
1264 fn lex_combinations(m: usize, k: usize) -> Vec<Vec<usize>> {
1265 let mut out = Vec::new();
1266 if k > m {
1267 return out;
1268 }
1269 let mut cur: Vec<usize> = (0..k).collect();
1270 loop {
1271 out.push(cur.clone());
1272 let mut advanced = false;
1273 let mut i = k;
1274 while i > 0 {
1275 i -= 1;
1276 if cur[i] != i + m - k {
1277 cur[i] += 1;
1278 for j in (i + 1)..k {
1279 cur[j] = cur[j - 1] + 1;
1280 }
1281 advanced = true;
1282 break;
1283 }
1284 }
1285 if !advanced {
1286 break;
1287 }
1288 }
1289 out
1290 }
1291 let subsets = lex_combinations(n, k);
1292 let canonicals: Vec<String> = subsets.iter().map(|c| tuple_canonical(c)).collect();
1293 let total = subsets.len();
1294 let mut node_order = NodeOrder::default();
1295 node_order.reserve(total);
1296 let mut node_attrs = Vec::with_capacity(total);
1297 let mut adj_indices = Vec::new();
1298 if 2 * k > n {
1299 return Self {
1301 mode,
1302 revision: u64::try_from(total).unwrap_or(u64::MAX),
1303 node_order: NodeOrder::from_names(canonicals),
1304 node_attrs: vec![AttrMap::new(); total],
1305 adj_indices: SlotRows::new(vec![Vec::new(); total]),
1306 all_int_cache: std::sync::Arc::default(),
1307 edge_slots_by_neighbor: std::sync::RwLock::default(),
1308 edge_index_endpoints: Vec::new(),
1309 edges: FxIndexMap::default(),
1310 runtime_policy: RuntimePolicy::new(mode),
1311 };
1312 }
1313 let mut binom = vec![vec![0usize; k + 1]; n + 1];
1315 for i in 0..=n {
1316 binom[i][0] = 1;
1317 for j in 1..=k.min(i) {
1318 binom[i][j] = if i == j {
1319 1
1320 } else {
1321 binom[i - 1][j - 1].saturating_add(binom[i - 1][j])
1322 };
1323 }
1324 }
1325 let lex_rank = |c: &[usize]| -> usize {
1326 let mut r = 0usize;
1327 let mut prev = 0usize;
1328 for (i, &ci) in c.iter().enumerate() {
1329 for j in prev..ci {
1330 r = r.saturating_add(binom[n - 1 - j][k - 1 - i]);
1331 }
1332 prev = ci + 1;
1333 }
1334 r
1335 };
1336 let mut seen = HashSet::<(usize, usize)>::with_capacity(total * 2);
1337 let mut node_idx: Vec<usize> = vec![usize::MAX; total];
1339 let mut in_s = vec![false; n];
1340 let m = n - k;
1341 let picks = lex_combinations(m, k);
1342 let mut edge_index_endpoints = Vec::new();
1343 let mut edges = FxIndexMap::default();
1344 for (s_idx, s) in subsets.iter().enumerate() {
1345 for &v in s {
1346 in_s[v] = true;
1347 }
1348 let complement: Vec<usize> = (0..n).filter(|&v| !in_s[v]).collect();
1349 for pick in &picks {
1350 let t: Vec<usize> = pick.iter().map(|&i| complement[i]).collect();
1351 let t_idx = lex_rank(&t);
1352 let pair = (s_idx.min(t_idx), s_idx.max(t_idx));
1353 if seen.insert(pair) {
1354 for idx in [s_idx, t_idx] {
1355 if node_idx[idx] == usize::MAX {
1356 let slot = node_order.push(canonicals[idx].clone());
1357 node_attrs.push(AttrMap::new());
1358 node_idx[idx] = slot;
1359 adj_indices.push(Vec::new());
1360 }
1361 }
1362 adj_indices[node_idx[s_idx]].push(node_idx[t_idx]);
1369 if node_idx[s_idx] != node_idx[t_idx] {
1370 adj_indices[node_idx[t_idx]].push(node_idx[s_idx]);
1371 }
1372 edge_index_endpoints.push((node_idx[s_idx], node_idx[t_idx]));
1373 edges.insert(
1374 Graph::canon_pair(node_idx[s_idx], node_idx[t_idx]),
1375 AttrMap::new(),
1376 );
1377 }
1378 }
1379 for &v in s {
1380 in_s[v] = false;
1381 }
1382 }
1383 let revision = u64::try_from(node_order.len() + edges.len()).unwrap_or(u64::MAX);
1384 Self {
1385 mode,
1386 revision,
1387 node_order,
1388 node_attrs,
1389 adj_indices: SlotRows::new(adj_indices),
1390 all_int_cache: std::sync::Arc::default(),
1391 edge_slots_by_neighbor: std::sync::RwLock::default(),
1392 edge_index_endpoints,
1393 edges,
1394 runtime_policy: RuntimePolicy::new(mode),
1395 }
1396 }
1397
1398 #[must_use]
1399 pub fn hardened() -> Self {
1400 Self::new(CompatibilityMode::Hardened)
1401 }
1402
1403 #[must_use]
1404 pub fn mode(&self) -> CompatibilityMode {
1405 self.mode
1406 }
1407
1408 #[must_use]
1409 pub fn node_count(&self) -> usize {
1410 self.node_order.len()
1411 }
1412
1413 #[must_use]
1414 pub fn edge_count(&self) -> usize {
1415 self.edges.len()
1416 }
1417
1418 #[must_use]
1419 pub fn revision(&self) -> u64 {
1420 self.revision
1421 }
1422
1423 #[must_use]
1424 pub fn has_node(&self, node: &str) -> bool {
1425 self.node_order.contains_key(node)
1426 }
1427
1428 #[inline]
1431 fn edge_pair_key(&self, left: &str, right: &str) -> Option<(usize, usize)> {
1432 let &l = self.node_order.get(left)?;
1433 let &r = self.node_order.get(right)?;
1434 Some(if l <= r { (l, r) } else { (r, l) })
1435 }
1436
1437 #[inline]
1438 fn canon_pair(l: usize, r: usize) -> (usize, usize) {
1439 if l <= r { (l, r) } else { (r, l) }
1440 }
1441
1442 #[must_use]
1467 pub fn induced_subgraph_ordered(&self, order: &[usize], runtime_policy: RuntimePolicy) -> Self {
1468 let mut parent_slot_to_subgraph_slot = vec![usize::MAX; self.node_order.slot_count()];
1469 for (slot, &parent_idx) in order.iter().enumerate() {
1470 let (_, parent_slot) = self
1471 .node_order
1472 .get_index(parent_idx)
1473 .expect("induced subgraph order must hold valid parent node indices");
1474 parent_slot_to_subgraph_slot[parent_slot] = slot;
1475 }
1476
1477 let mut node_order = NodeOrder::default();
1478 node_order.reserve(order.len());
1479 let mut node_attrs = Vec::with_capacity(order.len());
1480 for &parent_idx in order {
1481 let (name, parent_slot) = self
1482 .node_order
1483 .get_index(parent_idx)
1484 .expect("induced subgraph order must hold valid parent node indices");
1485 node_order.push(name.clone());
1486 node_attrs.push(self.node_attrs[parent_slot].clone());
1487 }
1488
1489 let mut adj_indices = vec![Vec::new(); order.len()];
1490 let mut edges = FxIndexMap::default();
1491 let mut edge_index_endpoints = Vec::new();
1492 for (slot, &parent_idx) in order.iter().enumerate() {
1493 let parent_slot = self.node_order.get_index(parent_idx).unwrap().1;
1494 for &parent_neighbor_slot in &self.adj_indices[parent_slot] {
1495 let other = parent_slot_to_subgraph_slot[parent_neighbor_slot];
1496 if other == usize::MAX || other < slot {
1497 continue;
1498 }
1499 let attrs = self
1500 .edges
1501 .get(&Self::canon_pair(parent_slot, parent_neighbor_slot))
1502 .cloned()
1503 .unwrap_or_default();
1504 edges.insert(Self::canon_pair(slot, other), attrs);
1505 let left_name = node_order.get_index(slot).expect("slot inserted above").0;
1506 let right_name = node_order.get_index(other).expect("slot inserted above").0;
1507 if left_name <= right_name {
1508 edge_index_endpoints.push((slot, other));
1509 } else {
1510 edge_index_endpoints.push((other, slot));
1511 }
1512 adj_indices[slot].push(other);
1513 if slot != other {
1514 adj_indices[other].push(slot);
1515 }
1516 }
1517 }
1518
1519 Self {
1520 mode: runtime_policy.mode(),
1521 revision: 0,
1522 node_order,
1523 node_attrs,
1524 adj_indices: SlotRows::new(adj_indices),
1525 all_int_cache: std::sync::Arc::default(),
1526 edge_slots_by_neighbor: std::sync::RwLock::default(),
1527 edge_index_endpoints,
1528 edges,
1529 runtime_policy,
1530 }
1531 }
1532
1533 #[must_use]
1534 pub fn has_edge(&self, left: &str, right: &str) -> bool {
1535 self.edge_pair_key(left, right)
1536 .is_some_and(|k| self.edges.contains_key(&k))
1537 }
1538
1539 #[must_use]
1546 pub fn node_index_matches_int(&self, idx: usize) -> bool {
1547 self.node_order
1548 .get_index(idx)
1549 .is_some_and(|(k, _)| k.parse::<usize>() == Ok(idx))
1550 }
1551
1552 #[must_use]
1556 pub fn node_names_are_positions(&self) -> bool {
1557 self.node_order.names_are_positions()
1558 }
1559
1560 #[must_use]
1564 pub fn has_edge_by_indices(&self, l: usize, r: usize) -> bool {
1565 let Some(ls) = self.node_order.slot_at(l) else {
1566 return false;
1567 };
1568 let Some(rs) = self.node_order.slot_at(r) else {
1569 return false;
1570 };
1571 self.edges.contains_key(&Self::canon_pair(ls, rs))
1572 }
1573
1574 #[must_use]
1575 pub fn nodes_ordered(&self) -> Vec<&str> {
1576 self.node_order.keys().map(String::as_str).collect()
1577 }
1578
1579 #[must_use]
1580 pub fn get_node_index(&self, node: &str) -> Option<usize> {
1581 self.node_order.get_index_of(node)
1582 }
1583
1584 #[must_use]
1585 pub fn get_node_name(&self, index: usize) -> Option<&str> {
1586 self.node_order.get_index(index).map(|(k, _)| k.as_str())
1587 }
1588
1589 #[must_use]
1590 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
1591 let &slot = self.node_order.get(node)?;
1598 Some(
1599 self.adj_indices[slot]
1600 .iter()
1601 .map(|&nbr_slot| {
1602 self.node_order
1603 .name(nbr_slot)
1604 .expect("adj_indices entries are valid node slots")
1605 })
1606 .collect::<Vec<&str>>(),
1607 )
1608 }
1609
1610 #[must_use]
1611 pub fn neighbors_iter(&self, node: &str) -> Option<impl Iterator<Item = &str> + '_> {
1612 let &slot = self.node_order.get(node)?;
1614 Some(self.adj_indices[slot].iter().map(move |&nbr_slot| {
1615 self.node_order
1616 .name(nbr_slot)
1617 .expect("adj_indices entries are valid node slots")
1618 }))
1619 }
1620
1621 #[must_use]
1625 pub fn incident_edge_attrs(
1626 &self,
1627 node: &str,
1628 ) -> Option<impl Iterator<Item = (bool, &AttrMap)> + '_> {
1629 let &slot = self.node_order.get(node)?;
1630 Some(self.adj_indices[slot].iter().map(move |&neighbour| {
1631 let attrs = self
1632 .edges
1633 .get(&Self::canon_pair(slot, neighbour))
1634 .expect("adjacency entries have matching edge storage");
1635 (neighbour == slot, attrs)
1636 }))
1637 }
1638
1639 #[must_use]
1642 #[inline]
1643 pub fn neighbors_indices(&self, node_idx: usize) -> Option<&[usize]> {
1644 if self.node_order.is_dense() {
1645 self.adj_indices.get(node_idx).map(Vec::as_slice)
1646 } else {
1647 self.adj_indices
1648 .positions(&self.node_order)
1649 .get(node_idx)
1650 .map(Vec::as_slice)
1651 }
1652 }
1653
1654 #[must_use]
1659 pub fn clone_with_fresh_policy(&self) -> Self {
1660 Self {
1661 mode: self.mode,
1662 revision: self.revision,
1663 node_order: self.node_order.clone(),
1664 node_attrs: self.node_attrs.clone(),
1665 adj_indices: self.adj_indices.clone(),
1666 all_int_cache: std::sync::Arc::default(),
1667 edge_slots_by_neighbor: std::sync::RwLock::default(),
1668 edge_index_endpoints: self.edge_index_endpoints.clone(),
1669 edges: self.edges.clone(),
1670 runtime_policy: RuntimePolicy::new(self.mode),
1671 }
1672 }
1673
1674 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)]) {
1680 let mut changed = false;
1682 for (node, order) in orders {
1683 let Some(&slot) = self.node_order.get(node.as_str()) else {
1684 continue;
1685 };
1686 let row = &self.adj_indices[slot];
1687 let row_set: std::collections::HashSet<usize> = row.iter().copied().collect();
1688 let mut new_row: Vec<usize> = Vec::with_capacity(row.len());
1689 let mut placed: std::collections::HashSet<usize> = std::collections::HashSet::new();
1690 for v in order {
1691 if let Some(&v_slot) = self.node_order.get(v.as_str())
1692 && row_set.contains(&v_slot)
1693 && placed.insert(v_slot)
1694 {
1695 new_row.push(v_slot);
1696 }
1697 }
1698 for &v_slot in row {
1699 if placed.insert(v_slot) {
1700 new_row.push(v_slot);
1701 }
1702 }
1703 if self.adj_indices[slot] != new_row {
1704 self.adj_indices.rows_mut()[slot] = new_row;
1705 changed = true;
1706 }
1707 }
1708 if changed {
1709 self.revision = self.revision.saturating_add(1);
1710 }
1711 }
1712
1713 pub fn reorder_rows_for_nx_copy_walk(&mut self) {
1724 self.ensure_compact();
1736 let n = self.adj_indices.len();
1737 let mut early: Vec<Vec<usize>> = vec![Vec::new(); n];
1738 for (v, row) in self.adj_indices.iter().enumerate() {
1739 for &pu in row {
1740 if pu > v {
1741 early[pu].push(v);
1742 }
1743 }
1744 }
1745 let mut new_rows: Vec<Vec<usize>> = Vec::with_capacity(n);
1746 for (pu, row) in self.adj_indices.iter().enumerate() {
1747 let mut new_row = std::mem::take(&mut early[pu]);
1748 new_row.reserve(row.len());
1749 for &v in row {
1750 if v >= pu {
1751 new_row.push(v);
1752 }
1753 }
1754 new_rows.push(new_row);
1755 }
1756 if *self.adj_indices != new_rows {
1757 *self.adj_indices.rows_mut() = new_rows;
1758 self.revision = self.revision.saturating_add(1);
1759 }
1760 }
1761
1762 pub fn reorder_rows_like(&mut self, source: &Self) {
1771 let mut to_source = vec![usize::MAX; self.node_order.slot_count()];
1772 let mut from_source = vec![usize::MAX; source.node_order.slot_count()];
1773 for (name, slot) in self.node_order.iter() {
1774 if let Some(&source_slot) = source.node_order.get(name) {
1775 to_source[slot] = source_slot;
1776 from_source[source_slot] = slot;
1777 }
1778 }
1779 let slots: Vec<usize> = self.node_order.values().collect();
1780 let changed = reorder_slot_rows_like(
1781 &mut self.adj_indices,
1782 &source.adj_indices,
1783 &slots,
1784 &to_source,
1785 &from_source,
1786 );
1787 if changed {
1788 self.revision = self.revision.saturating_add(1);
1789 }
1790 }
1791
1792 pub fn set_row_orders(&mut self, rows: &[Vec<usize>]) -> bool {
1798 let slots: Vec<usize> = self.node_order.values().collect();
1799 match set_slot_row_orders(&mut self.adj_indices, &slots, rows) {
1800 None => false,
1801 Some(changed) => {
1802 if changed {
1803 self.revision = self.revision.saturating_add(1);
1804 }
1805 true
1806 }
1807 }
1808 }
1809
1810 pub fn edges_storage_order_index_iter(
1812 &self,
1813 ) -> impl Iterator<Item = (usize, usize, &AttrMap)> + '_ {
1814 let slot_positions = self.node_order.slot_positions();
1815 self.edge_index_endpoints
1816 .iter()
1817 .copied()
1818 .zip(self.edges.values())
1819 .map(move |((left, right), attrs)| match &slot_positions {
1820 Some(positions) => (positions[left], positions[right], attrs),
1821 None => (left, right, attrs),
1822 })
1823 }
1824
1825 #[must_use]
1826 pub fn neighbor_count(&self, node: &str) -> usize {
1827 self.node_order
1829 .get(node)
1830 .map_or(0, |&slot| self.adj_indices[slot].len())
1831 }
1832
1833 #[must_use]
1834 pub fn neighbor_count_by_index(&self, idx: usize) -> usize {
1835 self.node_order
1836 .get_index(idx)
1837 .map_or(0, |(_, slot)| self.adj_indices[slot].len())
1838 }
1839
1840 #[must_use]
1843 pub fn degree(&self, node: &str) -> usize {
1844 let count = self.neighbor_count(node);
1845 if self.has_edge(node, node) {
1847 count + 1
1848 } else {
1849 count
1850 }
1851 }
1852
1853 #[must_use]
1858 pub fn degree_by_index(&self, idx: usize) -> usize {
1859 let mut count = self.neighbor_count_by_index(idx);
1860 if self.has_edge_by_indices(idx, idx) {
1861 count += 1; }
1863 count
1864 }
1865
1866 #[must_use]
1867 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
1868 let &slot = self.node_order.get(node)?;
1869 Some(&self.node_attrs[slot])
1870 }
1871
1872 #[must_use]
1873 #[inline]
1874 pub fn node_attrs_by_index(&self, idx: usize) -> Option<&AttrMap> {
1875 let slot = self.node_order.slot_at(idx)?;
1876 Some(&self.node_attrs[slot])
1877 }
1878
1879 #[must_use]
1880 pub fn edge_attrs(&self, left: &str, right: &str) -> Option<&AttrMap> {
1881 self.edges.get(&self.edge_pair_key(left, right)?)
1882 }
1883
1884 #[must_use]
1888 #[inline]
1889 pub fn edge_attrs_by_indices(&self, left_idx: usize, right_idx: usize) -> Option<&AttrMap> {
1890 let l_slot = self.node_order.slot_at(left_idx)?;
1891 let r_slot = self.node_order.slot_at(right_idx)?;
1892 self.edges.get(&Self::canon_pair(l_slot, r_slot))
1893 }
1894
1895 #[must_use]
1898 #[inline]
1899 pub fn edge_attrs_by_slot(&self, edge_slot: usize) -> Option<&AttrMap> {
1900 self.edges.get_index(edge_slot).map(|(_, attrs)| attrs)
1901 }
1902
1903 pub fn with_edge_slots_by_neighbor<T>(&self, f: impl FnOnce(&[Vec<usize>]) -> T) -> T {
1908 if let Ok(guard) = self.edge_slots_by_neighbor.read()
1909 && let Some((revision, rows)) = guard.as_ref()
1910 && *revision == self.revision
1911 {
1912 return f(rows);
1913 }
1914
1915 let mut guard = self
1916 .edge_slots_by_neighbor
1917 .write()
1918 .expect("edge slot cache lock poisoned");
1919 if !matches!(guard.as_ref(), Some((revision, _)) if *revision == self.revision) {
1920 let mut rows = Vec::with_capacity(self.node_order.len());
1921 for node in self.node_order.values() {
1922 let neighbors = &self.adj_indices[node];
1923 let mut slots = Vec::with_capacity(neighbors.len());
1924 for &neighbor in neighbors {
1925 let slot = self
1926 .edges
1927 .get_index_of(&Self::canon_pair(node, neighbor))
1928 .expect("adjacency entries have matching edge storage");
1929 slots.push(slot);
1930 }
1931 rows.push(slots);
1932 }
1933 *guard = Some((self.revision, rows));
1934 }
1935 let (_, rows) = guard
1936 .as_ref()
1937 .expect("edge slot cache is initialized before use");
1938 f(rows)
1939 }
1940
1941 #[must_use]
1947 pub fn any_edge_has_attr(&self, key: &str) -> bool {
1948 self.edges.values().any(|attrs| attrs.contains_key(key))
1949 }
1950
1951 #[must_use]
1957 pub fn all_edge_attr_values_scalar(&self) -> bool {
1958 self.edges.values().all(|attrs| {
1959 attrs.values().all(|v| {
1960 matches!(
1961 v,
1962 fnx_runtime::CgseValue::Int(_)
1963 | fnx_runtime::CgseValue::Float(_)
1964 | fnx_runtime::CgseValue::Bool(_)
1965 )
1966 })
1967 })
1968 }
1969
1970 #[must_use]
1973 pub fn any_node_has_attrs(&self) -> bool {
1974 self.node_order
1975 .values()
1976 .any(|slot| !self.node_attrs[slot].is_empty())
1977 }
1978
1979 #[must_use]
1982 pub fn any_edge_has_attrs(&self) -> bool {
1983 self.edges.values().any(|attrs| !attrs.is_empty())
1984 }
1985
1986 #[must_use]
1987 pub fn nodes_are_contiguous_int_prefix(&self) -> bool {
1988 self.node_order
1989 .keys()
1990 .enumerate()
1991 .all(|(index, key)| Self::decimal_key_matches_index(key, index))
1992 }
1993
1994 fn decimal_key_matches_index(key: &str, index: usize) -> bool {
1995 let bytes = key.as_bytes();
1996 if bytes.is_empty() || (bytes.len() > 1 && bytes[0] == b'0') {
1997 return false;
1998 }
1999
2000 let mut parsed = 0usize;
2001 for &byte in bytes {
2002 if !byte.is_ascii_digit() {
2003 return false;
2004 }
2005 let digit = usize::from(byte - b'0');
2006 let Some(next) = parsed
2007 .checked_mul(10)
2008 .and_then(|value| value.checked_add(digit))
2009 else {
2010 return false;
2011 };
2012 parsed = next;
2013 }
2014 parsed == index
2015 }
2016
2017 #[must_use]
2018 pub fn evidence_ledger(&self) -> &EvidenceLedger {
2019 self.runtime_policy.decision_log()
2020 }
2021
2022 pub fn drain_decision_records(&mut self) -> Vec<DecisionRecord> {
2023 self.runtime_policy.drain_decision_records()
2024 }
2025
2026 pub fn record_external_recovery(
2030 &mut self,
2031 operation: impl Into<std::borrow::Cow<'static, str>>,
2032 rationale: impl Into<std::borrow::Cow<'static, str>>,
2033 ) {
2034 self.runtime_policy.record(
2035 operation,
2036 DecisionAction::FullValidate,
2037 0.7,
2038 rationale,
2039 Vec::new(),
2040 );
2041 }
2042
2043 #[must_use]
2044 pub fn runtime_policy(&self) -> &RuntimePolicy {
2045 &self.runtime_policy
2046 }
2047
2048 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
2049 self.mode = runtime_policy.mode();
2050 self.runtime_policy = runtime_policy;
2051 }
2052
2053 #[must_use]
2055 pub fn is_directed(&self) -> bool {
2056 false
2057 }
2058
2059 #[must_use]
2061 pub fn is_multigraph(&self) -> bool {
2062 false
2063 }
2064
2065 pub fn add_node(&mut self, node: impl AsRef<str> + Into<String>) -> bool {
2066 self.add_node_with_attrs(node, AttrMap::new())
2067 }
2068
2069 fn add_node_with_attrs_unrecorded(
2092 &mut self,
2093 node: impl AsRef<str> + Into<String>,
2094 attrs: AttrMap,
2095 ) -> (bool, bool, usize, Option<usize>) {
2096 let node_ref = node.as_ref();
2097 if let Some(&slot) = self.node_order.get(node_ref) {
2098 let bucket = &mut self.node_attrs[slot];
2099 let changed = !attrs.is_empty()
2100 && attrs
2101 .iter()
2102 .any(|(key, value)| bucket.get(key) != Some(value));
2103 bucket.extend(attrs);
2104 if changed {
2105 self.revision = self.revision.saturating_add(1);
2106 }
2107 return (changed, true, bucket.len(), None);
2108 }
2109
2110 let slot = self.node_order.push(node.into());
2111 self.node_attrs.push(attrs);
2112 self.adj_indices.rows_mut().push(Vec::new());
2113 let attr_count = self.node_attrs[slot].len();
2114 self.revision = self.revision.saturating_add(1);
2115 (true, false, attr_count, Some(slot))
2116 }
2117
2118 fn resolve_empty_node_for_edge_unrecorded(&mut self, node: &str) -> (usize, bool) {
2134 if let Some(&slot) = self.node_order.get(node) {
2135 return (slot, false);
2136 }
2137 let slot = self.node_order.push(node.to_owned());
2138 self.node_attrs.push(AttrMap::new());
2139 self.adj_indices.rows_mut().push(Vec::new());
2140 self.revision = self.revision.saturating_add(1);
2141 (slot, true)
2142 }
2143
2144 pub fn add_node_with_attrs(
2145 &mut self,
2146 node: impl AsRef<str> + Into<String>,
2147 attrs: AttrMap,
2148 ) -> bool {
2149 self.compact_before_growth();
2150 let (changed, existed, attrs_count, _) = self.add_node_with_attrs_unrecorded(node, attrs);
2151 self.record_decision(
2152 "add_node",
2153 0.0,
2154 false,
2155 vec![
2156 EvidenceTerm {
2157 signal: "node_preexisting".into(),
2158 observed_value: bool_evidence(existed),
2159 log_likelihood_ratio: -3.0,
2160 },
2161 EvidenceTerm {
2162 signal: "attrs_count".into(),
2163 observed_value: count_evidence(attrs_count),
2164 log_likelihood_ratio: -1.0,
2165 },
2166 ],
2167 );
2168 changed
2169 }
2170
2171 #[must_use]
2174 pub fn extend_nodes_unrecorded<I, N>(&mut self, nodes: I) -> usize
2175 where
2176 I: IntoIterator<Item = N>,
2177 N: Into<String>,
2178 {
2179 self.compact_before_growth();
2180 let iterator = nodes.into_iter();
2181 let (lower_bound, _) = iterator.size_hint();
2182 self.node_order.reserve(lower_bound);
2183
2184 let mut inserted = 0usize;
2185 for node in iterator {
2186 let node = node.into();
2187 if self.node_order.contains_key(&node) {
2188 continue;
2189 }
2190 self.node_order.push(node);
2191 self.node_attrs.push(AttrMap::new());
2192 self.adj_indices.rows_mut().push(Vec::new());
2193 inserted += 1;
2194 }
2195 if inserted > 0 {
2196 self.revision = self
2197 .revision
2198 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
2199 self.record_decision(
2200 "extend_nodes_unrecorded",
2201 0.0,
2202 false,
2203 vec![EvidenceTerm {
2204 signal: "batch_node_count".into(),
2205 observed_value: count_evidence(inserted),
2206 log_likelihood_ratio: -1.0,
2207 }],
2208 );
2209 }
2210 inserted
2211 }
2212
2213 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
2217 where
2218 I: IntoIterator<Item = (String, AttrMap)>,
2219 {
2220 self.compact_before_growth();
2221 let iterator = nodes.into_iter();
2222 let (lower_bound, _) = iterator.size_hint();
2223 self.node_order.reserve(lower_bound);
2224
2225 let mut inserted = 0usize;
2226 for (node, attrs) in iterator {
2227 if let Some(&slot) = self.node_order.get(&node) {
2228 self.node_attrs[slot].extend(attrs);
2229 continue;
2230 }
2231 self.node_order.push(node);
2232 self.node_attrs.push(attrs);
2233 self.adj_indices.rows_mut().push(Vec::new());
2234 inserted += 1;
2235 }
2236 if inserted > 0 {
2237 self.revision = self
2238 .revision
2239 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
2240 self.record_decision(
2241 "extend_nodes_with_attrs_unrecorded",
2242 0.0,
2243 false,
2244 vec![EvidenceTerm {
2245 signal: "batch_node_count".into(),
2246 observed_value: count_evidence(inserted),
2247 log_likelihood_ratio: -1.0,
2248 }],
2249 );
2250 }
2251 inserted
2252 }
2253
2254 pub fn apply_node_defaults(&mut self, defaults: &AttrMap) -> bool {
2255 if defaults.is_empty() {
2256 return false;
2257 }
2258 let mut inserted = 0usize;
2259 for slot in self.node_order.values() {
2260 let attrs = &mut self.node_attrs[slot];
2261 for (key, value) in defaults {
2262 if !attrs.contains_key(key) {
2263 attrs.insert(key.clone(), value.clone());
2264 inserted += 1;
2265 }
2266 }
2267 }
2268 if inserted > 0 {
2269 self.revision = self.revision.saturating_add(1);
2270 self.record_decision(
2271 "apply_node_defaults",
2272 0.0,
2273 false,
2274 vec![EvidenceTerm {
2275 signal: "defaults_applied".into(),
2276 observed_value: count_evidence(inserted),
2277 log_likelihood_ratio: 0.0,
2278 }],
2279 );
2280 return true;
2281 }
2282 false
2283 }
2284
2285 pub fn add_edge(
2286 &mut self,
2287 left: impl AsRef<str>,
2288 right: impl AsRef<str>,
2289 ) -> Result<(), GraphError> {
2290 self.add_edge_with_attrs(left, right, AttrMap::new())
2291 }
2292
2293 #[must_use]
2308 pub fn extend_edges_unrecorded<I, L, R>(&mut self, edges: I) -> usize
2309 where
2310 I: IntoIterator<Item = (L, R)>,
2311 L: Into<String>,
2312 R: Into<String>,
2313 {
2314 self.compact_before_growth();
2315 let iterator = edges.into_iter();
2316 let (lower_bound, _) = iterator.size_hint();
2317 self.node_order.reserve(lower_bound);
2318 self.edges.reserve(lower_bound);
2319 self.edge_index_endpoints.reserve(lower_bound);
2320
2321 let mut inserted = 0usize;
2322 let mut nodes_added = false;
2323 for (left, right) in iterator {
2324 let left = left.into();
2325 let right = right.into();
2326 let (left_slot, l_added) = match self.node_order.get(&left) {
2327 Some(&slot) => (slot, false),
2328 None => {
2329 let slot = self.node_order.push(left.clone());
2330 self.node_attrs.push(AttrMap::new());
2331 self.adj_indices.rows_mut().push(Vec::new());
2332 (slot, true)
2333 }
2334 };
2335 nodes_added |= l_added;
2336
2337 let (right_slot, r_added) = if left == right {
2338 (left_slot, false)
2339 } else {
2340 match self.node_order.get(&right) {
2341 Some(&slot) => (slot, false),
2342 None => {
2343 let slot = self.node_order.push(right.clone());
2344 self.node_attrs.push(AttrMap::new());
2345 self.adj_indices.rows_mut().push(Vec::new());
2346 (slot, true)
2347 }
2348 }
2349 };
2350 nodes_added |= r_added;
2351
2352 let edge_key = Self::canon_pair(left_slot, right_slot);
2353 if self.edges.contains_key(&edge_key) {
2354 continue;
2355 }
2356 if left <= right {
2357 self.edge_index_endpoints.push((left_slot, right_slot));
2358 } else {
2359 self.edge_index_endpoints.push((right_slot, left_slot));
2360 }
2361 self.edges.insert(edge_key, AttrMap::new());
2362
2363 self.adj_indices.rows_mut()[left_slot].push(right_slot);
2364 if left_slot != right_slot {
2365 self.adj_indices.rows_mut()[right_slot].push(left_slot);
2366 }
2367 inserted += 1;
2368 }
2369 self.record_bulk_edge_summary(inserted, nodes_added);
2370 inserted
2371 }
2372
2373 #[must_use]
2379 pub fn extend_existing_index_edges_unrecorded<I>(&mut self, edges: I) -> usize
2380 where
2381 I: IntoIterator<Item = (usize, usize)>,
2382 {
2383 self.compact_before_growth();
2384 let iterator = edges.into_iter();
2385 let (lower_bound, _) = iterator.size_hint();
2386 self.edges.reserve(lower_bound);
2387 self.edge_index_endpoints.reserve(lower_bound);
2388
2389 let mut inserted = 0usize;
2390 for (left_idx, right_idx) in iterator {
2391 debug_assert!(left_idx < self.node_order.len());
2392 debug_assert!(right_idx < self.node_order.len());
2393 let left_slot = self
2394 .node_order
2395 .slot_at(left_idx)
2396 .expect("validated left node index");
2397 let right_slot = self
2398 .node_order
2399 .slot_at(right_idx)
2400 .expect("validated right node index");
2401
2402 let edge_key = Self::canon_pair(left_slot, right_slot);
2403 if self.edges.contains_key(&edge_key) {
2404 continue;
2405 }
2406
2407 let left_name = self
2408 .node_order
2409 .name(left_slot)
2410 .expect("validated left node index");
2411 let right_name = self
2412 .node_order
2413 .name(right_slot)
2414 .expect("validated right node index");
2415 if left_name <= right_name {
2416 self.edge_index_endpoints.push((left_slot, right_slot));
2417 } else {
2418 self.edge_index_endpoints.push((right_slot, left_slot));
2419 }
2420 self.edges.insert(edge_key, AttrMap::new());
2421
2422 self.adj_indices.rows_mut()[left_slot].push(right_slot);
2423 if left_slot != right_slot {
2424 self.adj_indices.rows_mut()[right_slot].push(left_slot);
2425 }
2426 inserted += 1;
2427 }
2428 self.record_bulk_edge_summary(inserted, false);
2429 inserted
2430 }
2431
2432 pub fn extend_existing_index_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
2440 where
2441 I: IntoIterator<Item = (usize, usize, AttrMap)>,
2442 {
2443 self.compact_before_growth();
2444 let iterator = edges.into_iter();
2445 let (lower_bound, _) = iterator.size_hint();
2446 self.edges.reserve(lower_bound);
2447 self.edge_index_endpoints.reserve(lower_bound);
2448
2449 let mut inserted = 0usize;
2450 for (left_idx, right_idx, attrs) in iterator {
2451 debug_assert!(left_idx < self.node_order.len());
2452 debug_assert!(right_idx < self.node_order.len());
2453 let left_slot = self
2454 .node_order
2455 .slot_at(left_idx)
2456 .expect("validated left node index");
2457 let right_slot = self
2458 .node_order
2459 .slot_at(right_idx)
2460 .expect("validated right node index");
2461
2462 let edge_key = Self::canon_pair(left_slot, right_slot);
2463 if let Some(existing) = self.edges.get_mut(&edge_key) {
2464 for (key, value) in attrs {
2466 existing.insert(key, value);
2467 }
2468 continue;
2469 }
2470
2471 let left_name = self
2472 .node_order
2473 .name(left_slot)
2474 .expect("validated left node index");
2475 let right_name = self
2476 .node_order
2477 .name(right_slot)
2478 .expect("validated right node index");
2479 if left_name <= right_name {
2480 self.edge_index_endpoints.push((left_slot, right_slot));
2481 } else {
2482 self.edge_index_endpoints.push((right_slot, left_slot));
2483 }
2484 self.edges.insert(edge_key, attrs);
2485
2486 self.adj_indices.rows_mut()[left_slot].push(right_slot);
2487 if left_slot != right_slot {
2488 self.adj_indices.rows_mut()[right_slot].push(left_slot);
2489 }
2490 inserted += 1;
2491 }
2492 self.record_bulk_edge_summary(inserted, false);
2493 inserted
2494 }
2495
2496 pub fn extend_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
2511 where
2512 I: IntoIterator<Item = (String, String, AttrMap)>,
2513 {
2514 self.compact_before_growth();
2515 let iterator = edges.into_iter();
2516 let (lower_bound, _) = iterator.size_hint();
2517 self.node_order.reserve(lower_bound);
2518 self.edges.reserve(lower_bound);
2519 self.edge_index_endpoints.reserve(lower_bound);
2520
2521 let mut inserted = 0usize;
2522 let mut nodes_added = false;
2523 let mut merged_changed = false;
2524 for (left, right, attrs) in iterator {
2525 let (left_slot, l_added) = match self.node_order.get(&left) {
2526 Some(&slot) => (slot, false),
2527 None => {
2528 let slot = self.node_order.push(left.clone());
2529 self.node_attrs.push(AttrMap::new());
2530 self.adj_indices.rows_mut().push(Vec::new());
2531 (slot, true)
2532 }
2533 };
2534 nodes_added |= l_added;
2535
2536 let (right_slot, r_added) = if left == right {
2537 (left_slot, false)
2538 } else {
2539 match self.node_order.get(&right) {
2540 Some(&slot) => (slot, false),
2541 None => {
2542 let slot = self.node_order.push(right.clone());
2543 self.node_attrs.push(AttrMap::new());
2544 self.adj_indices.rows_mut().push(Vec::new());
2545 (slot, true)
2546 }
2547 }
2548 };
2549 nodes_added |= r_added;
2550
2551 let edge_key = Self::canon_pair(left_slot, right_slot);
2552 if let Some(existing) = self.edges.get_mut(&edge_key) {
2553 if attrs.is_empty() {
2554 continue;
2555 }
2556 if attrs
2557 .iter()
2558 .any(|(key, value)| existing.get(key) != Some(value))
2559 {
2560 merged_changed = true;
2561 }
2562 existing.extend(attrs);
2563 continue;
2564 }
2565 if left <= right {
2566 self.edge_index_endpoints.push((left_slot, right_slot));
2567 } else {
2568 self.edge_index_endpoints.push((right_slot, left_slot));
2569 }
2570 self.edges.insert(edge_key, attrs);
2571
2572 self.adj_indices.rows_mut()[left_slot].push(right_slot);
2573 if left_slot != right_slot {
2574 self.adj_indices.rows_mut()[right_slot].push(left_slot);
2575 }
2576 inserted += 1;
2577 }
2578 if merged_changed {
2579 self.revision = self.revision.saturating_add(1);
2580 }
2581 self.record_bulk_edge_summary(inserted, nodes_added || merged_changed);
2582 inserted
2583 }
2584
2585 #[must_use]
2594 pub fn extend_fresh_index_edges_with_attrs_unrecorded<I, N>(
2595 &mut self,
2596 nodes: N,
2597 edges: I,
2598 ) -> usize
2599 where
2600 I: IntoIterator<Item = (usize, usize, AttrMap)>,
2601 N: IntoIterator<Item = String>,
2602 {
2603 if !self.node_order.is_empty()
2604 || !self.adj_indices.is_empty()
2605 || !self.edges.is_empty()
2606 || !self.edge_index_endpoints.is_empty()
2607 {
2608 return 0;
2609 }
2610
2611 let node_labels: Vec<String> = nodes.into_iter().collect();
2612 let node_count = node_labels.len();
2613 self.node_order = NodeOrder::from_names(node_labels.clone());
2614 self.node_attrs = vec![AttrMap::new(); node_count];
2615 *self.adj_indices.rows_mut() = vec![Vec::new(); node_count];
2616
2617 let iterator = edges.into_iter();
2618 let (lower_bound, _) = iterator.size_hint();
2619 self.edges.reserve(lower_bound);
2620 self.edge_index_endpoints.reserve(lower_bound);
2621
2622 let mut inserted = 0usize;
2623 let mut merged_changed = false;
2624 for (left_idx, right_idx, attrs) in iterator {
2625 if left_idx >= node_count || right_idx >= node_count {
2626 continue;
2627 }
2628
2629 let edge_key = Self::canon_pair(left_idx, right_idx);
2630 if let Some(existing) = self.edges.get_mut(&edge_key) {
2631 if attrs.is_empty() {
2632 continue;
2633 }
2634 if attrs
2635 .iter()
2636 .any(|(key, value)| existing.get(key) != Some(value))
2637 {
2638 merged_changed = true;
2639 }
2640 existing.extend(attrs);
2641 continue;
2642 }
2643
2644 if node_labels[left_idx] <= node_labels[right_idx] {
2645 self.edge_index_endpoints.push((left_idx, right_idx));
2646 } else {
2647 self.edge_index_endpoints.push((right_idx, left_idx));
2648 }
2649 self.edges.insert(edge_key, attrs);
2650 let rows = self.adj_indices.rows_mut();
2651 rows[left_idx].push(right_idx);
2652 if left_idx != right_idx {
2653 rows[right_idx].push(left_idx);
2654 }
2655 inserted += 1;
2656 }
2657
2658 self.record_bulk_edge_summary(inserted, node_count > 0 || merged_changed);
2659 inserted
2660 }
2661
2662 fn record_bulk_edge_summary(&mut self, inserted: usize, nodes_added: bool) {
2663 if inserted == 0 && !nodes_added {
2664 return;
2665 }
2666 self.revision = self
2667 .revision
2668 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
2669 self.record_decision(
2673 "extend_edges_unrecorded",
2674 0.0,
2675 false,
2676 vec![EvidenceTerm {
2677 signal: "batch_edge_count".into(),
2678 observed_value: count_evidence(inserted),
2679 log_likelihood_ratio: -1.0,
2680 }],
2681 );
2682 }
2683
2684 pub fn add_edge_with_attrs(
2699 &mut self,
2700 left: impl AsRef<str>,
2701 right: impl AsRef<str>,
2702 attrs: AttrMap,
2703 ) -> Result<(), GraphError> {
2704 self.compact_before_growth();
2705 let left = left.as_ref();
2706 let right = right.as_ref();
2707
2708 let unknown_feature = attrs
2709 .keys()
2710 .any(|key| key.starts_with("__fnx_incompatible"));
2711 let incompatibility_probability = if unknown_feature {
2712 1.0
2713 } else if left == right {
2714 0.22
2715 } else {
2716 0.08
2717 };
2718
2719 let action = self
2727 .runtime_policy
2728 .action_for(incompatibility_probability, unknown_feature);
2729
2730 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
2731 self.record_decision(
2732 "add_edge",
2733 incompatibility_probability,
2734 unknown_feature,
2735 vec![EvidenceTerm {
2736 signal: "unknown_incompatible_feature".into(),
2737 observed_value: bool_evidence(unknown_feature),
2738 log_likelihood_ratio: 12.0,
2739 }],
2740 );
2741 return Err(GraphError::FailClosed {
2742 operation: "add_edge",
2743 reason: "incompatible edge metadata".to_owned(),
2744 });
2745 }
2746
2747 let (left_key_idx, left_autocreated) = self.resolve_empty_node_for_edge_unrecorded(left);
2769 let (right_key_idx, right_autocreated) = if left == right {
2770 (left_key_idx, left_autocreated)
2771 } else {
2772 self.resolve_empty_node_for_edge_unrecorded(right)
2773 };
2774 let edge_key = Self::canon_pair(left_key_idx, right_key_idx);
2775 let self_loop = left == right;
2776 let (new_edge, changed, edge_attr_count) = match self.edges.entry(edge_key) {
2781 indexmap::map::Entry::Vacant(entry) => {
2782 let edge_attr_count = attrs.len();
2783 entry.insert(attrs);
2784 (true, true, edge_attr_count)
2785 }
2786 indexmap::map::Entry::Occupied(mut entry) => {
2787 let edge_attrs = entry.get_mut();
2788 let changed = !attrs.is_empty()
2789 && attrs
2790 .iter()
2791 .any(|(key, value)| edge_attrs.get(key) != Some(value));
2792 edge_attrs.extend(attrs);
2793 (false, changed, edge_attrs.len())
2794 }
2795 };
2796
2797 if changed {
2799 if new_edge {
2808 if left <= right {
2809 self.edge_index_endpoints
2810 .push((left_key_idx, right_key_idx));
2811 } else {
2812 self.edge_index_endpoints
2813 .push((right_key_idx, left_key_idx));
2814 }
2815 let rows = self.adj_indices.rows_mut();
2824 rows[left_key_idx].push(right_key_idx);
2825 if left_key_idx != right_key_idx {
2826 rows[right_key_idx].push(left_key_idx);
2827 }
2828 }
2829 self.revision = self.revision.saturating_add(1);
2830 }
2831
2832 self.record_decision(
2833 "add_edge",
2834 incompatibility_probability,
2835 unknown_feature,
2836 vec![
2837 EvidenceTerm {
2838 signal: "self_loop".into(),
2839 observed_value: bool_evidence(self_loop),
2840 log_likelihood_ratio: -0.5,
2841 },
2842 EvidenceTerm {
2843 signal: "edge_attr_count".into(),
2844 observed_value: count_evidence(edge_attr_count),
2845 log_likelihood_ratio: -2.0,
2846 },
2847 EvidenceTerm {
2848 signal: "left_autocreated".into(),
2849 observed_value: bool_evidence(left_autocreated),
2850 log_likelihood_ratio: -1.25,
2851 },
2852 EvidenceTerm {
2853 signal: "right_autocreated".into(),
2854 observed_value: bool_evidence(right_autocreated),
2855 log_likelihood_ratio: -1.25,
2856 },
2857 ],
2858 );
2859
2860 Ok(())
2861 }
2862
2863 pub fn apply_edge_defaults(&mut self, defaults: &AttrMap) -> bool {
2864 if defaults.is_empty() {
2865 return false;
2866 }
2867 let mut inserted = 0usize;
2868 for attrs in self.edges.values_mut() {
2869 for (key, value) in defaults {
2870 if !attrs.contains_key(key) {
2871 attrs.insert(key.clone(), value.clone());
2872 inserted += 1;
2873 }
2874 }
2875 }
2876 if inserted > 0 {
2877 self.revision = self.revision.saturating_add(1);
2878 self.record_decision(
2879 "apply_edge_defaults",
2880 0.0,
2881 false,
2882 vec![EvidenceTerm {
2883 signal: "defaults_applied".into(),
2884 observed_value: count_evidence(inserted),
2885 log_likelihood_ratio: 0.0,
2886 }],
2887 );
2888 return true;
2889 }
2890 false
2891 }
2892
2893 pub fn replace_edge_attrs(&mut self, left: &str, right: &str, attrs: AttrMap) -> bool {
2898 let Some(edge_key) = self.edge_pair_key(left, right) else {
2899 return false;
2900 };
2901 if let Some(slot) = self.edges.get_mut(&edge_key) {
2902 if *slot != attrs {
2903 *slot = attrs;
2904 self.revision = self.revision.saturating_add(1);
2905 }
2906 true
2907 } else {
2908 false
2909 }
2910 }
2911
2912 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
2914 if let Some(&slot) = self.node_order.get(node) {
2915 if self.node_attrs[slot] != attrs {
2916 self.node_attrs[slot] = attrs;
2917 self.revision = self.revision.saturating_add(1);
2918 }
2919 true
2920 } else {
2921 false
2922 }
2923 }
2924
2925 pub fn remove_edge(&mut self, left: &str, right: &str) -> bool {
2926 let (Some(&left_slot), Some(&right_slot)) =
2927 (self.node_order.get(left), self.node_order.get(right))
2928 else {
2929 return false;
2930 };
2931 let pair = Self::canon_pair(left_slot, right_slot);
2932 let removed = self.edges.swap_remove_full(&pair);
2933 if let Some((edge_pos, _, _)) = removed {
2934 let rows = self.adj_indices.rows_mut();
2935 rows[left_slot].retain(|&i| i != right_slot);
2936 if left_slot != right_slot {
2937 rows[right_slot].retain(|&i| i != left_slot);
2938 }
2939 self.edge_index_endpoints.swap_remove(edge_pos);
2940 self.revision = self.revision.saturating_add(1);
2941 true
2942 } else {
2943 false
2944 }
2945 }
2946
2947 pub fn remove_node(&mut self, node: &str) -> bool {
2948 let removed = self.remove_node_deferred_compaction(node);
2949 if removed {
2950 self.compact_when_sparse();
2951 }
2952 removed
2953 }
2954
2955 fn remove_node_deferred_compaction(&mut self, node: &str) -> bool {
2956 let Some((slot, popped)) = self.node_order.remove(node) else {
2959 return false;
2960 };
2961
2962 let rows = self.adj_indices.rows_mut();
2964 let nbrs = std::mem::take(&mut rows[slot]);
2965 for &nbr in &nbrs {
2966 let pair = Self::canon_pair(slot, nbr);
2967 if let Some((edge_pos, _, _)) = self.edges.swap_remove_full(&pair) {
2968 self.edge_index_endpoints.swap_remove(edge_pos);
2969 }
2970 if nbr != slot {
2971 rows[nbr].retain(|&i| i != slot);
2972 }
2973 }
2974
2975 if popped {
2976 self.node_attrs.pop();
2977 rows.pop();
2978 } else {
2979 self.node_attrs[slot].clear();
2980 }
2981
2982 self.revision = self.revision.saturating_add(1);
2983 true
2984 }
2985
2986 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
2987 where
2988 I: IntoIterator<Item = &'a str>,
2989 {
2990 let mut removed_nodes = 0usize;
2991 let old_edge_count = self.edges.len();
2992 for node in nodes {
2993 if self.remove_node_deferred_compaction(node) {
2994 removed_nodes += 1;
2995 }
2996 }
2997 let removed_edges = old_edge_count.saturating_sub(self.edges.len());
2998 if removed_nodes > 0 {
2999 self.compact_when_sparse();
3000 }
3001 (removed_nodes, removed_edges)
3002 }
3003
3004 fn compact_when_sparse(&mut self) {
3010 let removed = self.node_order.slot_count() - self.node_order.len();
3011 if removed > self.node_order.len() {
3012 self.compact_internal();
3013 }
3014 }
3015
3016 fn compact_before_growth(&mut self) {
3021 if !self.node_order.is_dense() {
3022 self.compact_internal();
3023 }
3024 }
3025
3026 #[allow(dead_code)]
3029 #[must_use]
3032 pub fn edge_weights_all_int(&self, attr: &str) -> bool {
3033 if let Ok(guard) = self.all_int_cache.read()
3034 && let Some((rev, a, v)) = guard.as_ref()
3035 && *rev == self.revision
3036 && a == attr
3037 {
3038 return *v;
3039 }
3040 let v = self
3041 .edges
3042 .values()
3043 .all(|attrs| attrs.get(attr).is_none_or(fnx_runtime::CgseValue::is_int));
3044 if let Ok(mut guard) = self.all_int_cache.write() {
3045 *guard = Some((self.revision, attr.to_owned(), v));
3046 }
3047 v
3048 }
3049
3050 pub fn edges_ordered(&self) -> Vec<EdgeSnapshot> {
3051 let mut ordered = Vec::with_capacity(self.edges.len());
3052 let mut seen = vec![false; self.node_order.slot_count()];
3053 for (u_name, u) in self.node_order.iter() {
3054 for &v in &self.adj_indices[u] {
3055 if !seen[v] {
3056 let pair = Self::canon_pair(u, v);
3057 if let Some(attrs) = self.edges.get(&pair) {
3058 let v_name = self.node_order.name(v).expect("valid node slot");
3059 ordered.push(EdgeSnapshot {
3060 left: u_name.clone(),
3061 right: v_name.to_owned(),
3062 attrs: attrs.clone(),
3063 });
3064 }
3065 }
3066 }
3067 seen[u] = true;
3068 }
3069
3070 ordered
3071 }
3072
3073 #[must_use]
3074 pub fn edges_ordered_indices_borrowed(&self) -> Vec<(usize, usize, &AttrMap)> {
3075 let mut ordered = Vec::with_capacity(self.edges.len());
3076 let mut seen = vec![false; self.node_order.slot_count()];
3077 let mut slot_to_pos = vec![0; self.node_order.slot_count()];
3078 for (pos, (_, slot)) in self.node_order.iter().enumerate() {
3079 slot_to_pos[slot] = pos;
3080 }
3081
3082 for (u_pos, (_, u)) in self.node_order.iter().enumerate() {
3083 for &v in &self.adj_indices[u] {
3084 if !seen[v] {
3085 let pair = Self::canon_pair(u, v);
3086 if let Some(attrs) = self.edges.get(&pair) {
3087 let v_pos = slot_to_pos[v];
3088 ordered.push((u_pos, v_pos, attrs));
3089 }
3090 }
3091 }
3092 seen[u] = true;
3093 }
3094
3095 ordered
3096 }
3097
3098 #[must_use]
3099 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, &AttrMap)> {
3100 let mut ordered = Vec::with_capacity(self.edges.len());
3101 let mut seen = vec![false; self.node_order.slot_count()];
3102 for (u_name, u) in self.node_order.iter() {
3103 for &v in &self.adj_indices[u] {
3104 if !seen[v] {
3105 let pair = Self::canon_pair(u, v);
3106 if let Some(attrs) = self.edges.get(&pair) {
3107 let v_name = self.node_order.name(v).expect("valid node slot");
3108 ordered.push((u_name.as_str(), v_name, attrs));
3109 }
3110 }
3111 }
3112 seen[u] = true;
3113 }
3114
3115 ordered
3116 }
3117
3118 #[must_use]
3129 pub fn weighted_size_int(&self, weight: &str) -> Option<i128> {
3130 let mut total: i128 = 0;
3131 for attrs in self.edges.values() {
3132 let value = match attrs.get(weight) {
3133 Some(CgseValue::Int(v)) => i128::from(*v),
3134 Some(_) => return None,
3135 None => 1,
3136 };
3137 total = total.checked_add(value)?;
3138 }
3139 Some(total)
3140 }
3141
3142 #[must_use]
3148 pub fn edges_ordered_indices(&self) -> Vec<(usize, usize)> {
3149 let mut ordered = Vec::with_capacity(self.edges.len());
3150 let mut seen_source = vec![false; self.node_order.slot_count()];
3151 let mut slot_to_pos = vec![0; self.node_order.slot_count()];
3152 for (pos, (_, slot)) in self.node_order.iter().enumerate() {
3153 slot_to_pos[slot] = pos;
3154 }
3155
3156 for (u_pos, (_, u)) in self.node_order.iter().enumerate() {
3157 for &v in &self.adj_indices[u] {
3158 if !seen_source[v] {
3159 let v_pos = slot_to_pos[v];
3160 ordered.push((u_pos, v_pos));
3161 }
3162 }
3163 seen_source[u] = true;
3164 }
3165 if ordered.len() == self.edges.len() {
3166 return ordered;
3167 }
3168 let mut present: std::collections::HashSet<(usize, usize)> =
3170 std::collections::HashSet::with_capacity(self.edges.len());
3171 for &(l, r) in &self.edge_index_endpoints {
3172 present.insert(Self::canon_pair(l, r));
3173 }
3174 let mut ordered = Vec::with_capacity(self.edges.len());
3175 let mut seen_pairs =
3176 std::collections::HashSet::<(usize, usize)>::with_capacity(self.edges.len());
3177 for (u_pos, (_, u)) in self.node_order.iter().enumerate() {
3178 for &v in &self.adj_indices[u] {
3179 let pair = Self::canon_pair(u, v);
3180 if !seen_pairs.insert(pair) {
3181 continue;
3182 }
3183 if present.contains(&pair) {
3184 let v_pos = slot_to_pos[v];
3185 ordered.push((u_pos, v_pos));
3186 }
3187 }
3188 }
3189 if ordered.len() < self.edges.len() {
3190 for &(l, r) in self.edges.keys() {
3191 if seen_pairs.insert((l, r)) {
3192 let l_pos = slot_to_pos[l];
3193 let r_pos = slot_to_pos[r];
3194 ordered.push((l_pos, r_pos));
3195 }
3196 }
3197 }
3198 ordered
3199 }
3200
3201 #[must_use]
3202 pub fn edges_storage_order_borrowed(&self) -> Vec<(&str, &str, &AttrMap)> {
3203 self.edge_index_endpoints
3204 .iter()
3205 .zip(self.edges.values())
3206 .map(|(&(l, r), attrs)| {
3207 (
3208 self.node_order.name(l).expect("valid index"),
3209 self.node_order.name(r).expect("valid index"),
3210 attrs,
3211 )
3212 })
3213 .collect()
3214 }
3215
3216 pub fn edges_storage_order_iter(&self) -> impl Iterator<Item = (&str, &str, &AttrMap)> + '_ {
3217 self.edge_index_endpoints
3218 .iter()
3219 .zip(self.edges.values())
3220 .map(|(&(l, r), attrs)| {
3221 (
3222 self.node_order.name(l).expect("valid index"),
3223 self.node_order.name(r).expect("valid index"),
3224 attrs,
3225 )
3226 })
3227 }
3228
3229 #[inline]
3232 #[must_use]
3233 pub fn is_compact(&self) -> bool {
3234 self.node_order.is_dense()
3235 }
3236
3237 pub fn ensure_compact(&mut self) {
3238 if self.is_compact() {
3239 return;
3240 }
3241 self.compact_internal();
3242 }
3243
3244 fn compact_internal(&mut self) {
3245 let n = self.node_order.len();
3246 let old_slots: Vec<usize> = self.node_order.values().collect();
3247 let old_to_new = self.node_order.compact();
3248
3249 let mut new_node_attrs = Vec::with_capacity(n);
3250 let mut new_adj_indices = Vec::with_capacity(n);
3251 let rows = self.adj_indices.rows_mut();
3252 for old_slot in old_slots {
3253 new_node_attrs.push(std::mem::take(&mut self.node_attrs[old_slot]));
3254 new_adj_indices.push(std::mem::take(&mut rows[old_slot]));
3255 }
3256
3257 for row in &mut new_adj_indices {
3259 for neighbor in row {
3260 *neighbor = old_to_new[*neighbor];
3261 }
3262 }
3263
3264 let old_edges = std::mem::take(&mut self.edges);
3266 let old_endpoints = std::mem::take(&mut self.edge_index_endpoints);
3267 let mut new_edges =
3268 FxIndexMap::with_capacity_and_hasher(old_edges.len(), rustc_hash::FxBuildHasher);
3269 let mut new_endpoints = Vec::with_capacity(old_endpoints.len());
3270
3271 for (((old_l, old_r), attrs), (ep_l, ep_r)) in old_edges.into_iter().zip(old_endpoints) {
3272 let new_l = old_to_new[old_l];
3273 let new_r = old_to_new[old_r];
3274 new_edges.insert(Self::canon_pair(new_l, new_r), attrs);
3275 new_endpoints.push((old_to_new[ep_l], old_to_new[ep_r]));
3276 }
3277
3278 self.node_attrs = new_node_attrs;
3279 *self.adj_indices.rows_mut() = new_adj_indices;
3280 self.edges = new_edges;
3281 self.edge_index_endpoints = new_endpoints;
3282 self.revision = self.revision.saturating_add(1);
3283 }
3284
3285 #[must_use]
3286 pub fn snapshot(&self) -> GraphSnapshot {
3287 let mut node_attrs: BTreeMap<String, AttrMap> = BTreeMap::new();
3288 for (name, slot) in self.node_order.iter() {
3289 if !self.node_attrs[slot].is_empty() {
3290 node_attrs.insert(name.clone(), self.node_attrs[slot].clone());
3291 }
3292 }
3293 GraphSnapshot {
3294 mode: self.mode,
3295 nodes: self.node_order.keys().cloned().collect(),
3296 node_attrs,
3297 edges: self.edges_ordered(),
3298 }
3299 }
3300
3301 fn record_decision(
3302 &mut self,
3303 operation: &'static str,
3304 incompatibility_probability: f64,
3305 unknown_incompatible_feature: bool,
3306 evidence: Vec<EvidenceTerm>,
3307 ) -> DecisionAction {
3308 let action = self
3309 .runtime_policy
3310 .action_for(incompatibility_probability, unknown_incompatible_feature);
3311 self.runtime_policy.record(
3312 operation,
3313 action,
3314 incompatibility_probability,
3315 "argmin expected loss over {allow,full_validate,fail_closed}",
3316 evidence,
3317 );
3318 action
3319 }
3320}
3321
3322#[derive(Debug, Default)]
3329struct IntAdjCache(std::sync::RwLock<Option<(u64, Vec<Vec<usize>>)>>);
3330
3331impl Clone for IntAdjCache {
3332 fn clone(&self) -> Self {
3333 Self::default()
3334 }
3335}
3336
3337enum IntAdjEdgeDelta<'a> {
3341 Add {
3342 left: &'a str,
3343 right: &'a str,
3344 left_new: bool,
3345 right_new: bool,
3346 pair_new: bool,
3347 },
3348 Remove {
3349 left: &'a str,
3350 right: &'a str,
3351 pair_gone: bool,
3352 },
3353}
3354
3355mod multigraph_storage {
3358 use super::{AttrMap, CompatibilityMode, MultiEdgeSnapshot, MultiGraphSnapshot, SlotOrder};
3359 use indexmap::IndexMap;
3360 #[cfg(test)]
3361 use indexmap::IndexSet;
3362
3363 #[cfg(test)]
3370 #[derive(Debug, Default)]
3371 pub struct MgIntStorageProto {
3372 pub node_names: Vec<String>,
3374 pub node_index: IndexMap<String, usize, rustc_hash::FxBuildHasher>,
3376 pub rows: Vec<IndexMap<usize, IndexSet<usize>>>,
3379 pub edges: IndexMap<(usize, usize), IndexMap<usize, AttrMap>, rustc_hash::FxBuildHasher>,
3381 pub edge_count: usize,
3382 }
3383
3384 #[cfg(test)]
3385 impl MgIntStorageProto {
3386 #[must_use]
3387 pub fn new() -> Self {
3388 Self::default()
3389 }
3390
3391 fn ensure_node(&mut self, name: &str) -> usize {
3392 if let Some(&idx) = self.node_index.get(name) {
3393 return idx;
3394 }
3395 let idx = self.node_names.len();
3396 self.node_names.push(name.to_owned());
3397 self.node_index.insert(name.to_owned(), idx);
3398 self.rows.push(IndexMap::new());
3399 idx
3400 }
3401
3402 pub fn add_keyed_edge(&mut self, left: &str, right: &str, key: usize, attrs: AttrMap) {
3406 let l = self.ensure_node(left);
3407 let r = if left == right {
3408 l
3409 } else {
3410 self.ensure_node(right)
3411 };
3412 let pair = (l.min(r), l.max(r));
3413 let bucket = self.edges.entry(pair).or_default();
3414 if !bucket.contains_key(&key) {
3415 self.edge_count += 1;
3416 }
3417 bucket.entry(key).or_default().extend(attrs);
3418 self.rows[l].entry(r).or_default().insert(key);
3419 if l != r {
3420 self.rows[r].entry(l).or_default().insert(key);
3421 }
3422 }
3423
3424 #[must_use]
3429 pub fn bulk_load_int_prefix<I>(node_count: usize, edges: I) -> Self
3430 where
3431 I: IntoIterator<Item = (usize, usize, usize)>,
3432 {
3433 let iterator = edges.into_iter();
3434 let (lower_bound, _) = iterator.size_hint();
3435 let mut proto = Self::new();
3436 proto.node_names.reserve(node_count);
3437 proto.node_index.reserve(node_count);
3438 proto.rows.reserve(node_count);
3439 proto.edges.reserve(lower_bound);
3440 for node in 0..node_count {
3441 let name = node.to_string();
3442 proto.node_names.push(name.clone());
3443 proto.node_index.insert(name, node);
3444 proto.rows.push(IndexMap::new());
3445 }
3446 for (left_idx, right_idx, key) in iterator {
3447 debug_assert!(left_idx < node_count);
3448 debug_assert!(right_idx < node_count);
3449 let pair = (left_idx.min(right_idx), left_idx.max(right_idx));
3450 proto
3451 .edges
3452 .entry(pair)
3453 .or_default()
3454 .insert(key, AttrMap::new());
3455 proto.rows[left_idx]
3456 .entry(right_idx)
3457 .or_default()
3458 .insert(key);
3459 if left_idx != right_idx {
3460 proto.rows[right_idx]
3461 .entry(left_idx)
3462 .or_default()
3463 .insert(key);
3464 }
3465 proto.edge_count += 1;
3466 }
3467 proto
3468 }
3469
3470 #[must_use]
3472 pub fn neighbor_names(&self, i: usize) -> Vec<&str> {
3473 self.rows[i]
3474 .keys()
3475 .map(|&j| self.node_names[j].as_str())
3476 .collect()
3477 }
3478
3479 #[must_use]
3481 pub fn key_order(&self, left: &str, right: &str) -> Vec<usize> {
3482 let (Some(&l), Some(&r)) = (self.node_index.get(left), self.node_index.get(right))
3483 else {
3484 return Vec::new();
3485 };
3486 self.edges
3487 .get(&(l.min(r), l.max(r)))
3488 .map(|bucket| bucket.keys().copied().collect())
3489 .unwrap_or_default()
3490 }
3491
3492 pub fn remove_edge(&mut self, left: &str, right: &str, key: Option<usize>) -> bool {
3498 let (Some(&l), Some(&r)) = (self.node_index.get(left), self.node_index.get(right))
3499 else {
3500 return false;
3501 };
3502 let pair = (l.min(r), l.max(r));
3503 let Some(bucket) = self.edges.get_mut(&pair) else {
3504 return false;
3505 };
3506 let Some(removal_key) = key.or_else(|| bucket.keys().next_back().copied()) else {
3507 return false;
3508 };
3509 if bucket.shift_remove(&removal_key).is_none() {
3510 return false;
3511 }
3512 self.edge_count -= 1;
3513 let pair_gone = bucket.is_empty();
3514 if pair_gone {
3515 self.edges.swap_remove(&pair);
3516 }
3517 let mut drop_cell = |u: usize, v: usize| {
3518 let mut cell_empty = false;
3519 if let Some(keys) = self.rows[u].get_mut(&v) {
3520 keys.shift_remove(&removal_key);
3521 cell_empty = keys.is_empty();
3522 }
3523 if cell_empty {
3524 self.rows[u].shift_remove(&v);
3525 }
3526 };
3527 drop_cell(l, r);
3528 if l != r {
3529 drop_cell(r, l);
3530 }
3531 true
3532 }
3533
3534 pub fn remove_node(&mut self, name: &str) -> bool {
3542 let Some(&idx) = self.node_index.get(name) else {
3543 return false;
3544 };
3545 self.node_names.remove(idx);
3546 self.node_index.shift_remove(name);
3547 for stored in self.node_index.values_mut() {
3548 if *stored > idx {
3549 *stored -= 1;
3550 }
3551 }
3552 self.rows.remove(idx);
3553 for row in &mut self.rows {
3554 let old = std::mem::take(row);
3555 for (j, keys) in old {
3556 if j == idx {
3557 continue;
3558 }
3559 row.insert(if j > idx { j - 1 } else { j }, keys);
3560 }
3561 }
3562 let old_edges = std::mem::take(&mut self.edges);
3563 let mut removed_cells = 0usize;
3564 for ((a, b), bucket) in old_edges {
3565 if a == idx || b == idx {
3566 removed_cells += bucket.len();
3567 continue;
3568 }
3569 self.edges.insert(
3570 (
3571 if a > idx { a - 1 } else { a },
3572 if b > idx { b - 1 } else { b },
3573 ),
3574 bucket,
3575 );
3576 }
3577 self.edge_count -= removed_cells;
3578 true
3579 }
3580 }
3581
3582 #[derive(Debug, Clone)]
3591 pub enum CompactKeys {
3592 One(usize),
3593 Many(crate::FxIndexSet<usize>),
3594 }
3595
3596 impl CompactKeys {
3597 pub fn insert(&mut self, key: usize) -> bool {
3599 match self {
3600 Self::One(existing) => {
3601 if *existing == key {
3602 return false;
3603 }
3604 let mut set =
3605 crate::FxIndexSet::with_capacity_and_hasher(2, rustc_hash::FxBuildHasher);
3606 set.insert(*existing);
3607 set.insert(key);
3608 *self = Self::Many(set);
3609 true
3610 }
3611 Self::Many(set) => set.insert(key),
3612 }
3613 }
3614
3615 #[cfg(test)]
3616 #[must_use]
3617 pub fn order(&self) -> Vec<usize> {
3618 match self {
3619 Self::One(key) => vec![*key],
3620 Self::Many(set) => set.iter().copied().collect(),
3621 }
3622 }
3623
3624 #[must_use]
3627 pub fn len(&self) -> usize {
3628 match self {
3629 Self::One(_) => 1,
3630 Self::Many(set) => set.len(),
3631 }
3632 }
3633
3634 pub fn shift_remove(&mut self, key: usize) -> (bool, bool) {
3638 match self {
3639 Self::One(existing) => {
3640 let removed = *existing == key;
3641 (removed, removed)
3642 }
3643 Self::Many(set) => {
3644 let removed = set.shift_remove(&key);
3645 (removed, set.is_empty())
3646 }
3647 }
3648 }
3649 }
3650
3651 #[derive(Debug, Clone)]
3657 pub enum CompactBucket {
3658 One(usize, AttrMap),
3659 Many(crate::FxIndexMap<usize, AttrMap>),
3660 }
3661
3662 impl CompactBucket {
3663 pub fn merge(&mut self, key: usize, attrs: AttrMap) -> bool {
3666 match self {
3667 Self::One(existing, existing_attrs) => {
3668 if *existing == key {
3669 existing_attrs.extend(attrs);
3670 return false;
3671 }
3672 let mut map =
3673 crate::FxIndexMap::with_capacity_and_hasher(2, rustc_hash::FxBuildHasher);
3674 map.insert(*existing, std::mem::take(existing_attrs));
3675 map.insert(key, attrs);
3676 *self = Self::Many(map);
3677 true
3678 }
3679 Self::Many(map) => match map.entry(key) {
3681 indexmap::map::Entry::Occupied(mut cell) => {
3682 cell.get_mut().extend(attrs);
3683 false
3684 }
3685 indexmap::map::Entry::Vacant(cell) => {
3686 cell.insert(attrs);
3687 true
3688 }
3689 },
3690 }
3691 }
3692
3693 #[must_use]
3694 pub fn key_order(&self) -> Vec<usize> {
3695 match self {
3696 Self::One(key, _) => vec![*key],
3697 Self::Many(map) => map.keys().copied().collect(),
3698 }
3699 }
3700
3701 #[must_use]
3702 pub fn attrs_for(&self, key: usize) -> Option<&AttrMap> {
3703 match self {
3704 Self::One(existing, attrs) => (*existing == key).then_some(attrs),
3705 Self::Many(map) => map.get(&key),
3706 }
3707 }
3708
3709 #[must_use]
3710 pub fn contains_key(&self, key: usize) -> bool {
3711 self.attrs_for(key).is_some()
3712 }
3713
3714 pub fn replace_attrs(&mut self, key: usize, attrs: AttrMap) -> bool {
3718 match self {
3719 Self::One(existing, existing_attrs) => {
3720 if *existing == key {
3721 *existing_attrs = attrs;
3722 true
3723 } else {
3724 false
3725 }
3726 }
3727 Self::Many(map) => {
3728 if let Some(slot) = map.get_mut(&key) {
3729 *slot = attrs;
3730 true
3731 } else {
3732 false
3733 }
3734 }
3735 }
3736 }
3737
3738 #[must_use]
3740 pub fn len(&self) -> usize {
3741 match self {
3742 Self::One(..) => 1,
3743 Self::Many(map) => map.len(),
3744 }
3745 }
3746
3747 #[must_use]
3748 pub fn is_empty(&self) -> bool {
3749 self.len() == 0
3750 }
3751
3752 #[must_use]
3755 pub fn last_key(&self) -> Option<usize> {
3756 match self {
3757 Self::One(key, _) => Some(*key),
3758 Self::Many(map) => map.keys().next_back().copied(),
3759 }
3760 }
3761
3762 pub fn shift_remove(&mut self, key: usize) -> (bool, bool) {
3765 match self {
3766 Self::One(existing, _) => {
3767 let removed = *existing == key;
3768 (removed, removed)
3769 }
3770 Self::Many(map) => {
3771 let removed = map.shift_remove(&key).is_some();
3772 (removed, map.is_empty())
3773 }
3774 }
3775 }
3776
3777 pub fn iter(&self) -> CompactBucketIter<'_> {
3780 match self {
3781 Self::One(key, attrs) => CompactBucketIter::One(std::iter::once((*key, attrs))),
3782 Self::Many(map) => CompactBucketIter::Many(map.iter()),
3783 }
3784 }
3785
3786 pub fn keys(&self) -> CompactBucketKeys<'_> {
3790 match self {
3791 Self::One(key, _) => CompactBucketKeys::One(Some(key)),
3792 Self::Many(map) => CompactBucketKeys::Many(map.keys()),
3793 }
3794 }
3795
3796 pub fn values(&self) -> CompactBucketValues<'_> {
3797 match self {
3798 Self::One(_, attrs) => CompactBucketValues::One(Some(attrs)),
3799 Self::Many(map) => CompactBucketValues::Many(map.values()),
3800 }
3801 }
3802 }
3803
3804 pub enum CompactBucketIter<'a> {
3807 One(std::iter::Once<(usize, &'a AttrMap)>),
3808 Many(indexmap::map::Iter<'a, usize, AttrMap>),
3809 }
3810
3811 impl<'a> Iterator for CompactBucketIter<'a> {
3812 type Item = (usize, &'a AttrMap);
3813
3814 fn next(&mut self) -> Option<Self::Item> {
3815 match self {
3816 Self::One(it) => it.next(),
3817 Self::Many(it) => it.next().map(|(key, attrs)| (*key, attrs)),
3818 }
3819 }
3820 }
3821
3822 pub enum CompactBucketKeys<'a> {
3826 One(Option<&'a usize>),
3827 Many(indexmap::map::Keys<'a, usize, AttrMap>),
3828 }
3829
3830 impl<'a> Iterator for CompactBucketKeys<'a> {
3831 type Item = &'a usize;
3832
3833 fn next(&mut self) -> Option<Self::Item> {
3834 match self {
3835 Self::One(opt) => opt.take(),
3836 Self::Many(keys) => keys.next(),
3837 }
3838 }
3839 }
3840
3841 pub enum CompactBucketValues<'a> {
3842 One(Option<&'a AttrMap>),
3843 Many(indexmap::map::Values<'a, usize, AttrMap>),
3844 }
3845
3846 impl<'a> Iterator for CompactBucketValues<'a> {
3847 type Item = &'a AttrMap;
3848
3849 fn next(&mut self) -> Option<Self::Item> {
3850 match self {
3851 Self::One(opt) => opt.take(),
3852 Self::Many(values) => values.next(),
3853 }
3854 }
3855 }
3856
3857 #[cfg(test)]
3863 #[derive(Debug, Default)]
3864 pub struct MgIntStorageProtoCompact {
3865 pub node_names: Vec<String>,
3866 pub node_index: IndexMap<String, usize, rustc_hash::FxBuildHasher>,
3867 pub rows: Vec<IndexMap<usize, CompactKeys>>,
3868 pub edges: IndexMap<(usize, usize), CompactBucket, rustc_hash::FxBuildHasher>,
3869 pub edge_count: usize,
3870 }
3871
3872 #[cfg(test)]
3873 impl MgIntStorageProtoCompact {
3874 #[must_use]
3875 pub fn new() -> Self {
3876 Self::default()
3877 }
3878
3879 fn ensure_node(&mut self, name: &str) -> usize {
3880 if let Some(&idx) = self.node_index.get(name) {
3881 return idx;
3882 }
3883 let idx = self.node_names.len();
3884 self.node_names.push(name.to_owned());
3885 self.node_index.insert(name.to_owned(), idx);
3886 self.rows.push(IndexMap::new());
3887 idx
3888 }
3889
3890 pub fn add_keyed_edge(
3892 &mut self,
3893 left: &str,
3894 right: &str,
3895 key: usize,
3896 attrs: AttrMap,
3897 ) -> bool {
3898 let l = self.ensure_node(left);
3899 let r = if left == right {
3900 l
3901 } else {
3902 self.ensure_node(right)
3903 };
3904 let pair = (l.min(r), l.max(r));
3905 let is_new = match self.edges.entry(pair) {
3906 indexmap::map::Entry::Occupied(mut bucket) => bucket.get_mut().merge(key, attrs),
3907 indexmap::map::Entry::Vacant(slot) => {
3908 slot.insert(CompactBucket::One(key, attrs));
3909 true
3910 }
3911 };
3912 if is_new {
3913 self.edge_count += 1;
3914 }
3915 match self.rows[l].entry(r) {
3916 indexmap::map::Entry::Occupied(mut cell) => {
3917 let _ = cell.get_mut().insert(key);
3918 }
3919 indexmap::map::Entry::Vacant(slot) => {
3920 slot.insert(CompactKeys::One(key));
3921 }
3922 }
3923 if l != r {
3924 match self.rows[r].entry(l) {
3925 indexmap::map::Entry::Occupied(mut cell) => {
3926 let _ = cell.get_mut().insert(key);
3927 }
3928 indexmap::map::Entry::Vacant(slot) => {
3929 slot.insert(CompactKeys::One(key));
3930 }
3931 }
3932 }
3933 is_new
3934 }
3935
3936 #[cfg(test)]
3938 #[must_use]
3939 pub fn bulk_load_int_prefix<I>(node_count: usize, edges: I) -> Self
3940 where
3941 I: IntoIterator<Item = (usize, usize, usize)>,
3942 {
3943 let iterator = edges.into_iter();
3944 let (lower_bound, _) = iterator.size_hint();
3945 let mut proto = Self::new();
3946 proto.node_names.reserve(node_count);
3947 proto.node_index.reserve(node_count);
3948 proto.rows.reserve(node_count);
3949 proto.edges.reserve(lower_bound);
3950 for node in 0..node_count {
3951 let name = node.to_string();
3952 proto.node_names.push(name.clone());
3953 proto.node_index.insert(name, node);
3954 proto.rows.push(IndexMap::new());
3955 }
3956 for (left_idx, right_idx, key) in iterator {
3957 debug_assert!(left_idx < node_count);
3958 debug_assert!(right_idx < node_count);
3959 let pair = (left_idx.min(right_idx), left_idx.max(right_idx));
3960 match proto.edges.entry(pair) {
3961 indexmap::map::Entry::Occupied(mut bucket) => {
3962 let _ = bucket.get_mut().merge(key, AttrMap::new());
3963 }
3964 indexmap::map::Entry::Vacant(slot) => {
3965 slot.insert(CompactBucket::One(key, AttrMap::new()));
3966 }
3967 }
3968 match proto.rows[left_idx].entry(right_idx) {
3969 indexmap::map::Entry::Occupied(mut cell) => {
3970 let _ = cell.get_mut().insert(key);
3971 }
3972 indexmap::map::Entry::Vacant(slot) => {
3973 slot.insert(CompactKeys::One(key));
3974 }
3975 }
3976 if left_idx != right_idx {
3977 match proto.rows[right_idx].entry(left_idx) {
3978 indexmap::map::Entry::Occupied(mut cell) => {
3979 let _ = cell.get_mut().insert(key);
3980 }
3981 indexmap::map::Entry::Vacant(slot) => {
3982 slot.insert(CompactKeys::One(key));
3983 }
3984 }
3985 }
3986 proto.edge_count += 1;
3987 }
3988 proto
3989 }
3990
3991 #[must_use]
3993 pub fn neighbor_names(&self, i: usize) -> Vec<&str> {
3994 self.rows[i]
3995 .keys()
3996 .map(|&j| self.node_names[j].as_str())
3997 .collect()
3998 }
3999
4000 #[must_use]
4002 pub fn key_order(&self, left: &str, right: &str) -> Vec<usize> {
4003 let (Some(&l), Some(&r)) = (self.node_index.get(left), self.node_index.get(right))
4004 else {
4005 return Vec::new();
4006 };
4007 self.edges
4008 .get(&(l.min(r), l.max(r)))
4009 .map(CompactBucket::key_order)
4010 .unwrap_or_default()
4011 }
4012 }
4013
4014 #[derive(Debug, Clone, Default)]
4023 pub struct MgSlabStorage {
4024 pub(crate) node_order: SlotOrder,
4026 pub node_attrs: Vec<AttrMap>,
4029 pub rows: Vec<crate::FxIndexMap<usize, CompactKeys>>,
4032 pub edges: IndexMap<(usize, usize), CompactBucket, rustc_hash::FxBuildHasher>,
4035 pub edge_count: usize,
4036 }
4037
4038 impl MgSlabStorage {
4039 #[must_use]
4040 pub fn new() -> Self {
4041 Self::default()
4042 }
4043
4044 fn ensure_node(&mut self, name: &str) -> usize {
4045 if let Some(&slot) = self.node_order.get(name) {
4046 return slot;
4047 }
4048 let (slot, fresh) = self.node_order.insert(name.to_owned());
4049 if fresh {
4050 self.rows.push(crate::FxIndexMap::default());
4051 self.node_attrs.push(AttrMap::new());
4052 } else {
4053 debug_assert!(self.rows[slot].is_empty());
4054 self.node_attrs[slot] = AttrMap::new();
4055 }
4056 slot
4057 }
4058
4059 pub fn add_keyed_edge(
4061 &mut self,
4062 left: &str,
4063 right: &str,
4064 key: usize,
4065 attrs: AttrMap,
4066 ) -> bool {
4067 let l = self.ensure_node(left);
4068 let r = if left == right {
4069 l
4070 } else {
4071 self.ensure_node(right)
4072 };
4073 self.add_keyed_edge_by_pair(l, r, key, attrs)
4074 }
4075
4076 pub fn add_keyed_edge_by_pair(
4082 &mut self,
4083 l: usize,
4084 r: usize,
4085 key: usize,
4086 attrs: AttrMap,
4087 ) -> bool {
4088 let pair = (l.min(r), l.max(r));
4089 let is_new = match self.edges.entry(pair) {
4090 indexmap::map::Entry::Occupied(mut bucket) => bucket.get_mut().merge(key, attrs),
4091 indexmap::map::Entry::Vacant(slot) => {
4092 slot.insert(CompactBucket::One(key, attrs));
4093 true
4094 }
4095 };
4096 if is_new {
4097 self.edge_count += 1;
4098 }
4099 match self.rows[l].entry(r) {
4100 indexmap::map::Entry::Occupied(mut cell) => {
4101 let _ = cell.get_mut().insert(key);
4102 }
4103 indexmap::map::Entry::Vacant(slot) => {
4104 slot.insert(CompactKeys::One(key));
4105 }
4106 }
4107 if l != r {
4108 match self.rows[r].entry(l) {
4109 indexmap::map::Entry::Occupied(mut cell) => {
4110 let _ = cell.get_mut().insert(key);
4111 }
4112 indexmap::map::Entry::Vacant(slot) => {
4113 slot.insert(CompactKeys::One(key));
4114 }
4115 }
4116 }
4117 is_new
4118 }
4119
4120 #[cfg(test)]
4123 #[must_use]
4124 pub fn bulk_load_int_prefix<I>(node_count: usize, edges: I) -> Self
4125 where
4126 I: IntoIterator<Item = (usize, usize, usize)>,
4127 {
4128 let iterator = edges.into_iter();
4129 let (lower_bound, _) = iterator.size_hint();
4130 let mut proto = Self::new();
4131 proto.node_order.reserve(node_count);
4132 proto.rows.reserve(node_count);
4133 proto.edges.reserve(lower_bound);
4134 proto.node_attrs.reserve(node_count);
4135 for node in 0..node_count {
4136 proto.node_order.insert(node.to_string());
4137 proto.rows.push(crate::FxIndexMap::default());
4138 proto.node_attrs.push(AttrMap::new());
4139 }
4140 for (left_slot, right_slot, key) in iterator {
4141 debug_assert!(left_slot < node_count);
4142 debug_assert!(right_slot < node_count);
4143 let pair = (left_slot.min(right_slot), left_slot.max(right_slot));
4144 match proto.edges.entry(pair) {
4145 indexmap::map::Entry::Occupied(mut bucket) => {
4146 let _ = bucket.get_mut().merge(key, AttrMap::new());
4147 }
4148 indexmap::map::Entry::Vacant(slot) => {
4149 slot.insert(CompactBucket::One(key, AttrMap::new()));
4150 }
4151 }
4152 match proto.rows[left_slot].entry(right_slot) {
4153 indexmap::map::Entry::Occupied(mut cell) => {
4154 let _ = cell.get_mut().insert(key);
4155 }
4156 indexmap::map::Entry::Vacant(slot) => {
4157 slot.insert(CompactKeys::One(key));
4158 }
4159 }
4160 if left_slot != right_slot {
4161 match proto.rows[right_slot].entry(left_slot) {
4162 indexmap::map::Entry::Occupied(mut cell) => {
4163 let _ = cell.get_mut().insert(key);
4164 }
4165 indexmap::map::Entry::Vacant(slot) => {
4166 slot.insert(CompactKeys::One(key));
4167 }
4168 }
4169 }
4170 proto.edge_count += 1;
4171 }
4172 proto
4173 }
4174
4175 pub fn remove_edge(&mut self, left: &str, right: &str, key: Option<usize>) -> bool {
4177 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
4178 else {
4179 return false;
4180 };
4181 let pair = (l.min(r), l.max(r));
4182 let Some(bucket) = self.edges.get_mut(&pair) else {
4183 return false;
4184 };
4185 let Some(removal_key) = key.or_else(|| bucket.last_key()) else {
4186 return false;
4187 };
4188 let (removed, bucket_empty) = bucket.shift_remove(removal_key);
4189 if !removed {
4190 return false;
4191 }
4192 self.edge_count -= 1;
4193 if bucket_empty {
4194 self.edges.swap_remove(&pair);
4195 }
4196 let mut drop_cell = |u: usize, v: usize| {
4197 let mut cell_empty = false;
4198 if let Some(keys) = self.rows[u].get_mut(&v) {
4199 let (cell_removed, now_empty) = keys.shift_remove(removal_key);
4200 debug_assert!(cell_removed);
4201 cell_empty = now_empty;
4202 }
4203 if cell_empty {
4204 self.rows[u].shift_remove(&v);
4205 }
4206 };
4207 drop_cell(l, r);
4208 if l != r {
4209 drop_cell(r, l);
4210 }
4211 true
4212 }
4213
4214 pub fn remove_node(&mut self, name: &str) -> bool {
4217 let Some(&slot) = self.node_order.get(name) else {
4218 return false;
4219 };
4220 let row = std::mem::take(&mut self.rows[slot]);
4221 for &nbr in row.keys() {
4222 if nbr != slot {
4223 self.rows[nbr].shift_remove(&slot);
4224 }
4225 if let Some(bucket) = self.edges.swap_remove(&(slot.min(nbr), slot.max(nbr))) {
4226 self.edge_count -= bucket.len();
4227 }
4228 }
4229 self.node_order.remove(name);
4230 self.node_attrs[slot] = AttrMap::new();
4231 true
4232 }
4233
4234 pub fn set_node_attrs(&mut self, name: &str, attrs: AttrMap) -> (bool, bool, usize) {
4237 let existed = self.node_order.contains_key(name);
4238 let changed = !existed
4239 || attrs.iter().any(|(key, value)| {
4240 self.node_attrs_for(name).and_then(|a| a.get(key)) != Some(value)
4241 });
4242 let slot = self.ensure_node(name);
4243 self.node_attrs[slot].extend(attrs);
4244 (existed, changed, self.node_attrs[slot].len())
4245 }
4246
4247 pub fn replace_node_attrs(&mut self, name: &str, attrs: AttrMap) {
4249 if let Some(&slot) = self.node_order.get(name) {
4250 self.node_attrs[slot] = attrs;
4251 }
4252 }
4253
4254 #[cfg(test)]
4257 #[must_use]
4258 pub fn neighbor_names(&self, pos: usize) -> Vec<&str> {
4259 let Some((_, slot)) = self.node_order.get_index(pos) else {
4260 return Vec::new();
4261 };
4262 self.rows[slot]
4263 .keys()
4264 .map(|&s| self.node_order.name(s).expect("row references a live slot"))
4265 .collect()
4266 }
4267
4268 #[must_use]
4271 pub fn neighbors_by_name(&self, name: &str) -> Option<Vec<&str>> {
4272 let &slot = self.node_order.get(name)?;
4273 Some(
4274 self.rows[slot]
4275 .keys()
4276 .map(|&s| self.node_order.name(s).expect("row references a live slot"))
4277 .collect(),
4278 )
4279 }
4280
4281 #[must_use]
4285 pub fn degree(&self, name: &str) -> usize {
4286 let Some(&slot) = self.node_order.get(name) else {
4287 return 0;
4288 };
4289 let mut deg = 0;
4290 for (&nbr_slot, keys) in &self.rows[slot] {
4291 let count = keys.len();
4292 if nbr_slot == slot {
4293 deg += count * 2;
4294 } else {
4295 deg += count;
4296 }
4297 }
4298 deg
4299 }
4300
4301 #[must_use]
4302 pub fn number_of_selfloops(&self) -> usize {
4303 self.edges
4304 .iter()
4305 .filter(|((left, right), _)| left == right)
4306 .map(|(_, bucket)| bucket.len())
4307 .sum()
4308 }
4309
4310 #[must_use]
4312 pub fn has_node(&self, name: &str) -> bool {
4313 self.node_order.contains_key(name)
4314 }
4315
4316 #[must_use]
4319 pub fn has_edge(&self, left: &str, right: &str) -> bool {
4320 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
4321 else {
4322 return false;
4323 };
4324 self.has_edge_by_pair(l, r)
4325 }
4326
4327 #[must_use]
4338 pub fn has_edge_by_pair(&self, l: usize, r: usize) -> bool {
4339 self.edges
4340 .get(&(l.min(r), l.max(r)))
4341 .is_some_and(|bucket| !bucket.is_empty())
4342 }
4343
4344 #[must_use]
4346 pub fn node_slot(&self, name: &str) -> Option<usize> {
4347 self.node_order.get(name).copied()
4348 }
4349
4350 #[must_use]
4360 pub fn slot_at_position(&self, position: usize) -> Option<usize> {
4361 self.node_order.slot_at(position)
4362 }
4363
4364 #[must_use]
4367 pub fn is_isolate(&self, name: &str) -> bool {
4368 self.node_order
4369 .get(name)
4370 .is_some_and(|&slot| self.rows[slot].is_empty())
4371 }
4372
4373 #[must_use]
4374 pub fn isolates(&self) -> Vec<String> {
4375 self.node_order
4376 .iter()
4377 .filter(|&(_, slot)| self.rows[slot].is_empty())
4378 .map(|(name, _)| name.clone())
4379 .collect()
4380 }
4381
4382 #[must_use]
4383 pub fn number_of_isolates(&self) -> usize {
4384 self.node_order
4385 .values()
4386 .filter(|&slot| self.rows[slot].is_empty())
4387 .count()
4388 }
4389
4390 #[must_use]
4395 pub fn weighted_size_int(&self, weight: &str) -> Option<i128> {
4396 let mut total: i128 = 0;
4397 for bucket in self.edges.values() {
4398 for (_key, attrs) in bucket.iter() {
4399 let value = match attrs.get(weight) {
4400 Some(fnx_runtime::CgseValue::Int(v)) => i128::from(*v),
4401 Some(_) => return None,
4402 None => 1,
4403 };
4404 total = total.checked_add(value)?;
4405 }
4406 }
4407 Some(total)
4408 }
4409
4410 #[must_use]
4412 pub fn key_order(&self, left: &str, right: &str) -> Vec<usize> {
4413 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
4414 else {
4415 return Vec::new();
4416 };
4417 self.edges
4418 .get(&(l.min(r), l.max(r)))
4419 .map(CompactBucket::key_order)
4420 .unwrap_or_default()
4421 }
4422
4423 #[must_use]
4424 pub fn edge_keys(&self, left: &str, right: &str) -> Option<Vec<usize>> {
4425 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
4426 else {
4427 return None;
4428 };
4429 self.edges
4430 .get(&(l.min(r), l.max(r)))
4431 .map(CompactBucket::key_order)
4432 }
4433
4434 pub fn edge_keys_iter(&self, left: &str, right: &str) -> Option<CompactBucketKeys<'_>> {
4435 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
4436 else {
4437 return None;
4438 };
4439 self.edges
4440 .get(&(l.min(r), l.max(r)))
4441 .map(CompactBucket::keys)
4442 }
4443
4444 pub fn edge_attr_values(&self, left: &str, right: &str) -> Option<CompactBucketValues<'_>> {
4445 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
4446 else {
4447 return None;
4448 };
4449 self.edges
4450 .get(&(l.min(r), l.max(r)))
4451 .map(CompactBucket::values)
4452 }
4453
4454 #[must_use]
4458 pub fn edge_attr_count_by_pair(&self, l: usize, r: usize) -> usize {
4459 self.edges
4460 .get(&(l.min(r), l.max(r)))
4461 .map_or(0, CompactBucket::len)
4462 }
4463
4464 #[must_use]
4468 pub fn next_edge_key_by_pair(&self, l: usize, r: usize) -> usize {
4469 let Some(bucket) = self.edges.get(&(l.min(r), l.max(r))) else {
4470 return 0;
4471 };
4472 let mut key = bucket.len();
4473 while bucket.contains_key(key) {
4474 key += 1;
4475 }
4476 key
4477 }
4478
4479 pub fn replace_edge_attrs(
4481 &mut self,
4482 left: &str,
4483 right: &str,
4484 key: usize,
4485 attrs: AttrMap,
4486 ) -> bool {
4487 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
4488 else {
4489 return false;
4490 };
4491 self.edges
4492 .get_mut(&(l.min(r), l.max(r)))
4493 .is_some_and(|bucket| bucket.replace_attrs(key, attrs))
4494 }
4495
4496 #[must_use]
4498 pub fn edge_attrs(&self, left: &str, right: &str, key: usize) -> Option<&AttrMap> {
4499 let (Some(&l), Some(&r)) = (self.node_order.get(left), self.node_order.get(right))
4500 else {
4501 return None;
4502 };
4503 self.edge_attrs_by_pair(l, r, key)
4504 }
4505
4506 #[must_use]
4508 pub fn edge_attrs_by_pair(&self, l: usize, r: usize, key: usize) -> Option<&AttrMap> {
4509 self.edges
4510 .get(&(l.min(r), l.max(r)))
4511 .and_then(|bucket| bucket.attrs_for(key))
4512 }
4513
4514 #[must_use]
4516 pub fn node_attrs_for(&self, name: &str) -> Option<&AttrMap> {
4517 let &slot = self.node_order.get(name)?;
4518 Some(&self.node_attrs[slot])
4519 }
4520
4521 #[must_use]
4522 pub fn get_node_index(&self, name: &str) -> Option<usize> {
4523 self.node_order.get_index_of(name)
4524 }
4525
4526 #[must_use]
4527 pub fn get_node_name(&self, index: usize) -> Option<&str> {
4528 self.node_order
4529 .get_index(index)
4530 .map(|(name, _)| name.as_str())
4531 }
4532
4533 #[must_use]
4534 pub fn node_index_matches_int(&self, index: usize) -> bool {
4535 self.node_order
4536 .get_index(index)
4537 .is_some_and(|(name, _)| name.parse::<usize>() == Ok(index))
4538 }
4539
4540 #[must_use]
4541 pub fn build_int_adjacency(&self) -> Vec<Vec<usize>> {
4542 let slot_to_position = self.slot_positions();
4543 self.node_order
4544 .values()
4545 .map(|slot| {
4546 self.rows[slot]
4547 .keys()
4548 .map(|&neighbor_slot| slot_to_position[neighbor_slot])
4549 .collect()
4550 })
4551 .collect()
4552 }
4553
4554 fn slot_positions(&self) -> Vec<usize> {
4557 let mut pos = vec![usize::MAX; self.node_order.slot_count()];
4558 for (p, slot) in self.node_order.values().enumerate() {
4559 pos[slot] = p;
4560 }
4561 pos
4562 }
4563
4564 #[must_use]
4571 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, usize, &AttrMap)> {
4572 let mut ordered = Vec::with_capacity(self.edge_count);
4573 let mut seen: std::collections::HashSet<(usize, usize, usize)> =
4574 std::collections::HashSet::with_capacity(self.edge_count);
4575 for (u_name, u_slot) in self.node_order.iter() {
4576 for &v_slot in self.rows[u_slot].keys() {
4577 let pair = (u_slot.min(v_slot), u_slot.max(v_slot));
4578 let Some(bucket) = self.edges.get(&pair) else {
4579 continue;
4580 };
4581 for (key, attrs) in bucket.iter() {
4582 if !seen.insert((pair.0, pair.1, key)) {
4583 continue;
4584 }
4585 let v_name = self
4586 .node_order
4587 .name(v_slot)
4588 .expect("row references a live slot");
4589 let (left, right) = if u_name.as_str() <= v_name {
4590 (u_name.as_str(), v_name)
4591 } else {
4592 (v_name, u_name.as_str())
4593 };
4594 ordered.push((left, right, key, attrs));
4595 }
4596 }
4597 }
4598 ordered
4599 }
4600
4601 #[must_use]
4602 pub fn snapshot(&self, mode: CompatibilityMode) -> MultiGraphSnapshot {
4603 let node_attrs = self
4604 .node_order
4605 .iter()
4606 .filter_map(|(name, slot)| {
4607 let attrs = &self.node_attrs[slot];
4608 (!attrs.is_empty()).then(|| (name.clone(), attrs.clone()))
4609 })
4610 .collect();
4611 let edges = self
4612 .edges_ordered_borrowed()
4613 .into_iter()
4614 .map(|(left, right, key, attrs)| MultiEdgeSnapshot {
4615 left: left.to_owned(),
4616 right: right.to_owned(),
4617 key,
4618 attrs: attrs.clone(),
4619 })
4620 .collect();
4621 MultiGraphSnapshot {
4622 mode,
4623 nodes: self.node_order.keys().cloned().collect(),
4624 node_attrs,
4625 edges,
4626 }
4627 }
4628
4629 #[must_use]
4640 pub fn edges_ordered_indices_borrowed(&self) -> Vec<(usize, usize, usize, &AttrMap)> {
4641 let slot_to_pos = self.slot_positions();
4642 let mut ordered = Vec::with_capacity(self.edge_count);
4643 let mut seen: std::collections::HashSet<(usize, usize, usize)> =
4644 std::collections::HashSet::with_capacity(self.edge_count);
4645 for (u_name, u_slot) in self.node_order.iter() {
4646 for &v_slot in self.rows[u_slot].keys() {
4647 let pair = (u_slot.min(v_slot), u_slot.max(v_slot));
4648 let Some(bucket) = self.edges.get(&pair) else {
4649 continue;
4650 };
4651 for (key, attrs) in bucket.iter() {
4652 if !seen.insert((pair.0, pair.1, key)) {
4653 continue;
4654 }
4655 let v_name = self
4656 .node_order
4657 .name(v_slot)
4658 .expect("row references a live slot");
4659 let (left_pos, right_pos) = if u_name.as_str() <= v_name {
4660 (slot_to_pos[u_slot], slot_to_pos[v_slot])
4661 } else {
4662 (slot_to_pos[v_slot], slot_to_pos[u_slot])
4663 };
4664 ordered.push((left_pos, right_pos, key, attrs));
4665 }
4666 }
4667 }
4668 ordered
4669 }
4670
4671 #[cfg(test)]
4678 #[must_use]
4679 pub fn edges_ordered_names(&self) -> Vec<(&str, &str, usize)> {
4680 let mut ordered = Vec::with_capacity(self.edge_count);
4681 let mut seen: std::collections::HashSet<(usize, usize, usize)> =
4682 std::collections::HashSet::with_capacity(self.edge_count);
4683 for (u_name, u_slot) in self.node_order.iter() {
4684 for &v_slot in self.rows[u_slot].keys() {
4685 let pair = (u_slot.min(v_slot), u_slot.max(v_slot));
4686 let Some(bucket) = self.edges.get(&pair) else {
4687 continue;
4688 };
4689 for key in bucket.key_order() {
4690 if !seen.insert((pair.0, pair.1, key)) {
4691 continue;
4692 }
4693 let v_name = self
4694 .node_order
4695 .name(v_slot)
4696 .expect("row references a live slot");
4697 let (left, right) = if u_name.as_str() <= v_name {
4698 (u_name.as_str(), v_name)
4699 } else {
4700 (v_name, u_name.as_str())
4701 };
4702 ordered.push((left, right, key));
4703 }
4704 }
4705 }
4706 ordered
4707 }
4708
4709 pub fn clear_edges(&mut self) {
4710 self.edges.clear();
4711 for row in &mut self.rows {
4712 row.clear();
4713 }
4714 self.edge_count = 0;
4715 }
4716
4717 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
4718 where
4719 I: IntoIterator<Item = &'a str>,
4720 {
4721 let remove_slots: rustc_hash::FxHashSet<usize> = nodes
4722 .into_iter()
4723 .filter_map(|name| self.node_order.get(name).copied())
4724 .collect();
4725 if remove_slots.is_empty() {
4726 return (0, 0);
4727 }
4728
4729 let old_edge_count = self.edge_count;
4730 self.edges.retain(|(left, right), _| {
4731 !remove_slots.contains(left) && !remove_slots.contains(right)
4732 });
4733 self.edge_count = self.edges.values().map(CompactBucket::len).sum();
4734
4735 for (slot, row) in self.rows.iter_mut().enumerate() {
4736 if remove_slots.contains(&slot) {
4737 row.clear();
4738 } else {
4739 row.retain(|neighbor, _| !remove_slots.contains(neighbor));
4740 }
4741 }
4742 self.node_order.remove_slots(&remove_slots);
4743 for &slot in &remove_slots {
4744 self.node_attrs[slot].clear();
4745 }
4746 (
4747 remove_slots.len(),
4748 old_edge_count.saturating_sub(self.edge_count),
4749 )
4750 }
4751
4752 pub fn reorder_rows_for_nx_copy_walk(&mut self) {
4760 let pos = self.slot_positions();
4761 let order_slots: Vec<usize> = self.node_order.values().collect();
4762 let mut new_orders: Vec<(usize, Vec<usize>)> = Vec::with_capacity(order_slots.len());
4763 for &u_slot in &order_slots {
4764 let pu = pos[u_slot];
4765 let mut early: Vec<(usize, usize, usize)> = Vec::new();
4766 let mut late: Vec<usize> = Vec::new();
4767 for &v_slot in self.rows[u_slot].keys() {
4768 let pv = pos[v_slot];
4769 if pv < pu {
4770 let idx = self.rows[v_slot]
4771 .get_index_of(&u_slot)
4772 .unwrap_or(usize::MAX);
4773 early.push((pv, idx, v_slot));
4774 } else {
4775 late.push(v_slot);
4776 }
4777 }
4778 early.sort_unstable();
4779 let mut order: Vec<usize> = early.into_iter().map(|(_, _, v)| v).collect();
4780 order.extend(late);
4781 new_orders.push((u_slot, order));
4782 }
4783 for (u_slot, order) in new_orders {
4784 let mut old = std::mem::take(&mut self.rows[u_slot]);
4785 let mut rebuilt = crate::FxIndexMap::with_capacity_and_hasher(
4786 old.len(),
4787 rustc_hash::FxBuildHasher,
4788 );
4789 for v_slot in order {
4790 if let Some(cell) = old.shift_remove(&v_slot) {
4791 rebuilt.insert(v_slot, cell);
4792 }
4793 }
4794 debug_assert!(old.is_empty(), "copy-walk reorder dropped a cell");
4795 self.rows[u_slot] = rebuilt;
4796 }
4797 }
4798
4799 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)]) {
4802 for (node, order) in orders {
4803 let Some(&slot) = self.node_order.get(node.as_str()) else {
4804 continue;
4805 };
4806 let mut old = std::mem::take(&mut self.rows[slot]);
4807 let mut new_row = crate::FxIndexMap::with_capacity_and_hasher(
4808 old.len(),
4809 rustc_hash::FxBuildHasher,
4810 );
4811 for v in order {
4812 if let Some(&v_slot) = self.node_order.get(v.as_str())
4813 && let Some((k, val)) = old.shift_remove_entry(&v_slot)
4814 {
4815 new_row.insert(k, val);
4816 }
4817 }
4818 for (k, val) in old {
4819 new_row.insert(k, val);
4820 }
4821 self.rows[slot] = new_row;
4822 }
4823 }
4824
4825 pub fn reorder_rows_like(&mut self, source: &Self) -> bool {
4831 let mut to_source = vec![usize::MAX; self.node_order.slot_count()];
4832 for (name, slot) in self.node_order.iter() {
4833 if let Some(&source_slot) = source.node_order.get(name) {
4834 to_source[slot] = source_slot;
4835 }
4836 }
4837 let mut changed = false;
4838 for slot in self.node_order.values() {
4839 let row = &mut self.rows[slot];
4840 let Some(source_row) = source.rows.get(to_source[slot]) else {
4841 continue;
4842 };
4843 if row.len() < 2 {
4844 continue;
4845 }
4846 let rank = |w: &usize| {
4847 source_row
4848 .get_index_of(&to_source[*w])
4849 .unwrap_or(usize::MAX)
4850 };
4851 if !row.keys().map(rank).is_sorted() {
4852 row.sort_by_cached_key(|w, _| rank(w));
4853 changed = true;
4854 }
4855 }
4856 changed
4857 }
4858 }
4859}
4860
4861#[derive(Debug, Clone)]
4862pub struct MultiGraph {
4863 mode: CompatibilityMode,
4864 revision: u64,
4865 storage: multigraph_storage::MgSlabStorage,
4866 runtime_policy: RuntimePolicy,
4867 edge_count: usize,
4868 int_adj_cache: IntAdjCache,
4870}
4871
4872pub struct MgNeighborNames<'a> {
4874 rows: indexmap::map::Keys<'a, usize, multigraph_storage::CompactKeys>,
4875 slot_names: &'a [Option<String>],
4876}
4877
4878impl<'a> Iterator for MgNeighborNames<'a> {
4879 type Item = &'a str;
4880
4881 fn next(&mut self) -> Option<Self::Item> {
4882 self.rows.next().map(|&slot| {
4883 self.slot_names[slot]
4884 .as_deref()
4885 .expect("row references a live slot")
4886 })
4887 }
4888}
4889
4890pub struct MgEdgeKeys<'a>(multigraph_storage::CompactBucketKeys<'a>);
4892
4893impl<'a> Iterator for MgEdgeKeys<'a> {
4894 type Item = &'a usize;
4895
4896 fn next(&mut self) -> Option<Self::Item> {
4897 self.0.next()
4898 }
4899}
4900
4901impl MultiGraph {
4902 #[must_use]
4905 pub fn clone_with_fresh_policy(&self) -> Self {
4906 Self {
4907 mode: self.mode,
4908 revision: self.revision,
4909 storage: self.storage.clone(),
4910 runtime_policy: RuntimePolicy::new(self.mode),
4911 edge_count: self.edge_count,
4912 int_adj_cache: IntAdjCache::default(),
4913 }
4914 }
4915
4916 pub fn reorder_rows_for_nx_copy_walk(&mut self) {
4923 self.storage.reorder_rows_for_nx_copy_walk();
4924 *self
4925 .int_adj_cache
4926 .0
4927 .get_mut()
4928 .expect("int_adj_cache poisoned") = None;
4929 }
4930
4931 pub fn apply_row_orders(&mut self, orders: &[(String, Vec<String>)]) {
4935 self.storage.apply_row_orders(orders);
4936 *self
4941 .int_adj_cache
4942 .0
4943 .get_mut()
4944 .expect("int_adj_cache poisoned") = None;
4945 }
4946
4947 pub fn reorder_rows_like(&mut self, source: &Self) {
4954 if self.storage.reorder_rows_like(&source.storage) {
4955 *self
4956 .int_adj_cache
4957 .0
4958 .get_mut()
4959 .expect("int_adj_cache poisoned") = None;
4960 }
4961 }
4962
4963 #[must_use]
4964 pub fn new(mode: CompatibilityMode) -> Self {
4965 Self {
4966 mode,
4967 revision: 0,
4968 storage: multigraph_storage::MgSlabStorage::new(),
4969 runtime_policy: RuntimePolicy::new(mode),
4970 edge_count: 0,
4971 int_adj_cache: IntAdjCache::default(),
4972 }
4973 }
4974
4975 #[must_use]
4976 pub fn with_runtime_policy(runtime_policy: RuntimePolicy) -> Self {
4977 let mode = runtime_policy.mode();
4978 Self {
4979 mode,
4980 revision: 0,
4981 storage: multigraph_storage::MgSlabStorage::new(),
4982 runtime_policy,
4983 edge_count: 0,
4984 int_adj_cache: IntAdjCache::default(),
4985 }
4986 }
4987
4988 fn slab_store(&self) -> &multigraph_storage::MgSlabStorage {
4989 &self.storage
4990 }
4991
4992 fn slab_store_mut(&mut self) -> &mut multigraph_storage::MgSlabStorage {
4993 &mut self.storage
4994 }
4995
4996 #[must_use]
4997 pub fn strict() -> Self {
4998 Self::new(CompatibilityMode::Strict)
4999 }
5000
5001 #[must_use]
5002 pub fn hardened() -> Self {
5003 Self::new(CompatibilityMode::Hardened)
5004 }
5005
5006 #[must_use]
5007 pub fn mode(&self) -> CompatibilityMode {
5008 self.mode
5009 }
5010
5011 #[must_use]
5012 pub fn node_count(&self) -> usize {
5013 self.slab_store().node_order.len()
5014 }
5015
5016 #[must_use]
5017 pub fn edge_count(&self) -> usize {
5018 self.edge_count
5019 }
5020
5021 #[must_use]
5022 pub fn number_of_selfloops(&self) -> usize {
5023 self.slab_store().number_of_selfloops()
5024 }
5025
5026 #[must_use]
5028 pub fn edge_keys(&self, left: &str, right: &str) -> Option<Vec<usize>> {
5029 self.slab_store().edge_keys(left, right)
5030 }
5031
5032 pub fn edge_keys_iter(&self, left: &str, right: &str) -> Option<MgEdgeKeys<'_>> {
5034 self.slab_store()
5035 .edge_keys_iter(left, right)
5036 .map(MgEdgeKeys)
5037 }
5038
5039 #[must_use]
5040 pub fn revision(&self) -> u64 {
5041 self.revision
5042 }
5043
5044 #[must_use]
5045 pub fn has_node(&self, node: &str) -> bool {
5046 self.slab_store().has_node(node)
5047 }
5048
5049 #[must_use]
5050 pub fn has_edge(&self, left: &str, right: &str) -> bool {
5051 self.slab_store().has_edge(left, right)
5052 }
5053
5054 #[must_use]
5085 pub fn has_edge_by_indices(&self, li: usize, ri: usize) -> bool {
5086 let store = self.slab_store();
5087 match (store.slot_at_position(li), store.slot_at_position(ri)) {
5088 (Some(left), Some(right)) => store.has_edge_by_pair(left, right),
5089 _ => false,
5090 }
5091 }
5092
5093 #[must_use]
5106 pub fn edge_attrs_by_indices(&self, li: usize, ri: usize, key: usize) -> Option<&AttrMap> {
5107 let store = self.slab_store();
5108 match (store.slot_at_position(li), store.slot_at_position(ri)) {
5109 (Some(left), Some(right)) => store.edge_attrs_by_pair(left, right, key),
5110 _ => None,
5111 }
5112 }
5113
5114 #[must_use]
5115 pub fn nodes_ordered(&self) -> Vec<&str> {
5116 self.slab_store()
5117 .node_order
5118 .keys()
5119 .map(String::as_str)
5120 .collect()
5121 }
5122
5123 #[must_use]
5126 #[inline]
5127 pub fn get_node_index(&self, node: &str) -> Option<usize> {
5128 self.slab_store().get_node_index(node)
5129 }
5130
5131 #[must_use]
5133 #[inline]
5134 pub fn get_node_name(&self, index: usize) -> Option<&str> {
5135 self.slab_store().get_node_name(index)
5136 }
5137
5138 #[must_use]
5141 pub fn node_index_matches_int(&self, idx: usize) -> bool {
5142 self.slab_store().node_index_matches_int(idx)
5143 }
5144
5145 #[must_use]
5150 pub fn node_names_are_positions(&self) -> bool {
5151 self.slab_store().node_order.names_are_positions()
5152 }
5153
5154 #[must_use]
5155 pub fn neighbors(&self, node: &str) -> Option<Vec<&str>> {
5156 self.slab_store().neighbors_by_name(node)
5157 }
5158
5159 pub fn neighbors_iter(&self, node: &str) -> Option<MgNeighborNames<'_>> {
5160 let slab = self.slab_store();
5161 let &slot = slab.node_order.get(node)?;
5162 Some(MgNeighborNames {
5163 rows: slab.rows[slot].keys(),
5164 slot_names: slab.node_order.names(),
5165 })
5166 }
5167
5168 fn build_int_adjacency(&self) -> Vec<Vec<usize>> {
5174 self.slab_store().build_int_adjacency()
5175 }
5176
5177 pub fn with_int_adjacency<R>(&self, f: impl FnOnce(&[Vec<usize>]) -> R) -> R {
5185 {
5186 let guard = self.int_adj_cache.0.read().expect("int_adj_cache poisoned");
5187 if let Some((rev, adj)) = guard.as_ref()
5188 && *rev == self.revision
5189 {
5190 return f(adj);
5191 }
5192 }
5193 let adj = self.build_int_adjacency();
5194 let result = f(&adj);
5195 *self
5196 .int_adj_cache
5197 .0
5198 .write()
5199 .expect("int_adj_cache poisoned") = Some((self.revision, adj));
5200 result
5201 }
5202
5203 fn advance_int_adj_memo(&mut self, prev_revision: u64, delta: IntAdjEdgeDelta<'_>) {
5213 let needs_indices = matches!(
5214 delta,
5215 IntAdjEdgeDelta::Add { pair_new: true, .. }
5216 | IntAdjEdgeDelta::Remove {
5217 pair_gone: true,
5218 ..
5219 }
5220 );
5221 let indices = if needs_indices {
5222 let (left, right) = match &delta {
5223 IntAdjEdgeDelta::Add { left, right, .. }
5224 | IntAdjEdgeDelta::Remove { left, right, .. } => (*left, *right),
5225 };
5226 match (
5227 self.storage.get_node_index(left),
5228 self.storage.get_node_index(right),
5229 ) {
5230 (Some(u), Some(v)) => Some((u, v)),
5231 _ => None,
5232 }
5233 } else {
5234 None
5235 };
5236 let Ok(slot) = self.int_adj_cache.0.get_mut() else {
5237 return;
5238 };
5239 if needs_indices && indices.is_none() {
5240 *slot = None;
5243 return;
5244 }
5245 let Some((rev, rows)) = slot.as_mut() else {
5246 return;
5247 };
5248 if *rev != prev_revision {
5249 return;
5250 }
5251 let mut advanced = true;
5252 match delta {
5253 IntAdjEdgeDelta::Add {
5254 left,
5255 right,
5256 left_new,
5257 right_new,
5258 pair_new,
5259 } => {
5260 if left_new {
5261 rows.push(Vec::new());
5262 }
5263 if right_new && left != right {
5264 rows.push(Vec::new());
5265 }
5266 if pair_new && let Some((u, v)) = indices {
5267 rows[u].push(v);
5268 if u != v {
5269 rows[v].push(u);
5270 }
5271 }
5272 }
5273 IntAdjEdgeDelta::Remove { pair_gone, .. } => {
5274 if pair_gone && let Some((u, v)) = indices {
5275 if let Some(pu) = rows[u].iter().position(|&x| x == v) {
5278 rows[u].remove(pu);
5279 if u != v {
5280 if let Some(pv) = rows[v].iter().position(|&x| x == u) {
5281 rows[v].remove(pv);
5282 } else {
5283 advanced = false;
5284 }
5285 }
5286 } else {
5287 advanced = false;
5288 }
5289 }
5290 }
5291 }
5292 if advanced {
5293 *rev = self.revision;
5294 } else {
5295 *slot = None;
5296 }
5297 }
5298
5299 #[must_use]
5300 pub fn edge_keys_vec(&self, left: &str, right: &str) -> Vec<usize> {
5301 self.slab_store().key_order(left, right)
5302 }
5303
5304 #[must_use]
5305 pub fn node_attrs(&self, node: &str) -> Option<&AttrMap> {
5306 self.slab_store().node_attrs_for(node)
5307 }
5308
5309 #[must_use]
5310 pub fn edge_attrs(&self, left: &str, right: &str, key: usize) -> Option<&AttrMap> {
5311 self.slab_store().edge_attrs(left, right, key)
5312 }
5313
5314 pub fn edge_attr_values(
5324 &self,
5325 left: &str,
5326 right: &str,
5327 ) -> Option<impl Iterator<Item = &AttrMap> + '_> {
5328 self.slab_store().edge_attr_values(left, right)
5329 }
5330
5331 #[must_use]
5332 pub fn evidence_ledger(&self) -> &EvidenceLedger {
5333 self.runtime_policy.decision_log()
5334 }
5335
5336 pub fn drain_decision_records(&mut self) -> Vec<DecisionRecord> {
5337 self.runtime_policy.drain_decision_records()
5338 }
5339
5340 pub fn record_external_recovery(
5344 &mut self,
5345 operation: impl Into<std::borrow::Cow<'static, str>>,
5346 rationale: impl Into<std::borrow::Cow<'static, str>>,
5347 ) {
5348 self.runtime_policy.record(
5349 operation,
5350 DecisionAction::FullValidate,
5351 0.7,
5352 rationale,
5353 Vec::new(),
5354 );
5355 }
5356
5357 #[must_use]
5358 pub fn runtime_policy(&self) -> &RuntimePolicy {
5359 &self.runtime_policy
5360 }
5361
5362 pub fn set_runtime_policy(&mut self, runtime_policy: RuntimePolicy) {
5363 self.mode = runtime_policy.mode();
5364 self.runtime_policy = runtime_policy;
5365 }
5366
5367 #[must_use]
5368 pub fn is_directed(&self) -> bool {
5369 false
5370 }
5371
5372 #[must_use]
5373 pub fn is_multigraph(&self) -> bool {
5374 true
5375 }
5376
5377 #[must_use]
5380 pub fn degree(&self, node: &str) -> usize {
5381 self.slab_store().degree(node)
5382 }
5383
5384 #[must_use]
5392 pub fn isolates(&self) -> Vec<String> {
5393 self.slab_store().isolates()
5394 }
5395
5396 #[must_use]
5398 pub fn number_of_isolates(&self) -> usize {
5399 self.slab_store().number_of_isolates()
5400 }
5401
5402 #[must_use]
5405 pub fn is_isolate(&self, node: &str) -> bool {
5406 self.slab_store().is_isolate(node)
5407 }
5408
5409 pub fn add_node(&mut self, node: impl AsRef<str>) -> bool {
5410 self.add_node_with_attrs(node, AttrMap::new())
5411 }
5412
5413 fn add_node_with_attrs_unrecorded(
5421 &mut self,
5422 node: impl AsRef<str>,
5423 attrs: AttrMap,
5424 ) -> (bool, bool, usize) {
5425 let (existed, changed, attrs_count) = self.storage.set_node_attrs(node.as_ref(), attrs);
5426 if changed {
5427 self.revision = self.revision.saturating_add(1);
5428 }
5429 (changed, existed, attrs_count)
5430 }
5431
5432 pub fn add_node_with_attrs(&mut self, node: impl AsRef<str>, attrs: AttrMap) -> bool {
5433 let (changed, existed, attrs_count) = self.add_node_with_attrs_unrecorded(node, attrs);
5434 self.record_decision(
5435 "add_node",
5436 0.0,
5437 false,
5438 vec![
5439 EvidenceTerm {
5440 signal: "node_preexisting".into(),
5441 observed_value: bool_evidence(existed),
5442 log_likelihood_ratio: -3.0,
5443 },
5444 EvidenceTerm {
5445 signal: "attrs_count".into(),
5446 observed_value: count_evidence(attrs_count),
5447 log_likelihood_ratio: -1.0,
5448 },
5449 ],
5450 );
5451 changed
5452 }
5453
5454 pub fn add_edge(
5455 &mut self,
5456 left: impl Into<String>,
5457 right: impl Into<String>,
5458 ) -> Result<usize, GraphError> {
5459 self.add_edge_impl(left, right, None, AttrMap::new())
5460 }
5461
5462 pub fn add_edge_with_attrs(
5463 &mut self,
5464 left: impl Into<String>,
5465 right: impl Into<String>,
5466 attrs: AttrMap,
5467 ) -> Result<usize, GraphError> {
5468 self.add_edge_impl(left, right, None, attrs)
5469 }
5470
5471 pub fn add_edge_with_key_and_attrs(
5472 &mut self,
5473 left: impl Into<String>,
5474 right: impl Into<String>,
5475 key: usize,
5476 attrs: AttrMap,
5477 ) -> Result<usize, GraphError> {
5478 self.add_edge_impl(left, right, Some(key), attrs)
5479 }
5480
5481 #[must_use]
5484 pub fn add_fresh_edge_unrecorded(
5485 &mut self,
5486 left: impl Into<String>,
5487 right: impl Into<String>,
5488 ) -> Option<usize> {
5489 self.add_fresh_edge_with_key_unrecorded(left, right, 0)
5490 }
5491
5492 #[must_use]
5496 pub fn add_fresh_edge_with_key_unrecorded(
5497 &mut self,
5498 left: impl Into<String>,
5499 right: impl Into<String>,
5500 key: usize,
5501 ) -> Option<usize> {
5502 let left = left.into();
5503 let right = right.into();
5504 self.storage.set_node_attrs(&left, AttrMap::new());
5505 if left != right {
5506 self.storage.set_node_attrs(&right, AttrMap::new());
5507 }
5508
5509 if self.storage.has_edge(&left, &right) {
5510 return None;
5511 }
5512 let inserted = self
5513 .storage
5514 .add_keyed_edge(&left, &right, key, AttrMap::new());
5515 debug_assert!(inserted, "fresh edge unexpectedly merged");
5516 self.edge_count += 1;
5517 self.revision = self.revision.saturating_add(1);
5518 Some(key)
5519 }
5520
5521 pub fn extend_nodes_with_attrs_unrecorded<I>(&mut self, nodes: I) -> usize
5525 where
5526 I: IntoIterator<Item = (String, AttrMap)>,
5527 {
5528 let mut inserted = 0usize;
5529 for (node, attrs) in nodes {
5530 let (existed, _, _) = self.storage.set_node_attrs(&node, attrs);
5531 if !existed {
5532 inserted += 1;
5533 }
5534 }
5535 if inserted > 0 {
5536 self.revision = self
5537 .revision
5538 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
5539 self.record_decision(
5540 "extend_nodes_with_attrs_unrecorded",
5541 0.0,
5542 false,
5543 vec![EvidenceTerm {
5544 signal: "batch_node_count".into(),
5545 observed_value: count_evidence(inserted),
5546 log_likelihood_ratio: -1.0,
5547 }],
5548 );
5549 }
5550 inserted
5551 }
5552
5553 pub fn extend_nodes_unrecorded<I, N>(&mut self, nodes: I) -> usize
5555 where
5556 I: IntoIterator<Item = N>,
5557 N: AsRef<str>,
5558 {
5559 let mut inserted = 0usize;
5560 for node in nodes {
5561 let (existed, _, _) = self.storage.set_node_attrs(node.as_ref(), AttrMap::new());
5562 if !existed {
5563 inserted += 1;
5564 }
5565 }
5566 if inserted > 0 {
5567 self.revision = self
5568 .revision
5569 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
5570 self.record_decision(
5571 "extend_nodes_unrecorded",
5572 0.0,
5573 false,
5574 vec![EvidenceTerm {
5575 signal: "batch_node_count".into(),
5576 observed_value: count_evidence(inserted),
5577 log_likelihood_ratio: -1.0,
5578 }],
5579 );
5580 }
5581 inserted
5582 }
5583
5584 pub fn extend_keyed_edges_with_attrs_unrecorded<I>(&mut self, edges: I) -> usize
5591 where
5592 I: IntoIterator<Item = (String, String, usize, AttrMap)>,
5593 {
5594 let mut inserted = 0usize;
5595 for (left, right, key, attrs) in edges {
5596 if self.storage.add_keyed_edge(&left, &right, key, attrs) {
5597 self.edge_count += 1;
5598 inserted += 1;
5599 }
5600 }
5601 if inserted > 0 {
5602 self.revision = self
5603 .revision
5604 .saturating_add(u64::try_from(inserted).unwrap_or(u64::MAX));
5605 self.record_decision(
5606 "extend_keyed_edges_with_attrs_unrecorded",
5607 0.0,
5608 false,
5609 vec![EvidenceTerm {
5610 signal: "batch_edge_count".into(),
5611 observed_value: count_evidence(inserted),
5612 log_likelihood_ratio: -1.0,
5613 }],
5614 );
5615 }
5616 inserted
5617 }
5618
5619 #[must_use]
5626 pub fn extend_fresh_int_prefix_keyed_edges_unrecorded<I>(
5627 &mut self,
5628 node_count: usize,
5629 edges: I,
5630 ) -> usize
5631 where
5632 I: IntoIterator<Item = (usize, usize, usize)>,
5633 {
5634 debug_assert_eq!(self.node_count(), 0);
5635 debug_assert_eq!(self.edge_count(), 0);
5636
5637 let iterator = edges.into_iter();
5638 let (lower_bound, _) = iterator.size_hint();
5639
5640 let node_names = (0..node_count)
5641 .map(|node| node.to_string())
5642 .collect::<Vec<_>>();
5643 let mut fresh_slab = {
5644 use multigraph_storage::MgSlabStorage;
5645 let mut slab = MgSlabStorage::new();
5646 slab.node_order.reserve(node_count);
5647 slab.node_attrs.reserve(node_count);
5648 slab.rows.reserve(node_count);
5649 slab.edges.reserve(lower_bound);
5650 for name in &node_names {
5651 slab.node_order.insert(name.clone());
5652 slab.node_attrs.push(AttrMap::new());
5653 slab.rows.push(crate::FxIndexMap::default());
5654 }
5655 slab
5656 };
5657
5658 let mut inserted = 0usize;
5659 for (left_idx, right_idx, key) in iterator {
5660 debug_assert!(left_idx < node_names.len());
5661 debug_assert!(right_idx < node_names.len());
5662 {
5663 use multigraph_storage::{CompactBucket, CompactKeys};
5664 let pair = (left_idx.min(right_idx), left_idx.max(right_idx));
5665 match fresh_slab.edges.entry(pair) {
5666 indexmap::map::Entry::Occupied(mut bucket) => {
5667 let _ = bucket.get_mut().merge(key, AttrMap::new());
5668 }
5669 indexmap::map::Entry::Vacant(slot) => {
5670 slot.insert(CompactBucket::One(key, AttrMap::new()));
5671 }
5672 }
5673 match fresh_slab.rows[left_idx].entry(right_idx) {
5674 indexmap::map::Entry::Occupied(mut cell) => {
5675 let _ = cell.get_mut().insert(key);
5676 }
5677 indexmap::map::Entry::Vacant(slot) => {
5678 slot.insert(CompactKeys::One(key));
5679 }
5680 }
5681 if left_idx != right_idx {
5682 match fresh_slab.rows[right_idx].entry(left_idx) {
5683 indexmap::map::Entry::Occupied(mut cell) => {
5684 let _ = cell.get_mut().insert(key);
5685 }
5686 indexmap::map::Entry::Vacant(slot) => {
5687 slot.insert(CompactKeys::One(key));
5688 }
5689 }
5690 }
5691 fresh_slab.edge_count += 1;
5692 }
5693
5694 inserted += 1;
5695 }
5696 self.edge_count = fresh_slab.edge_count;
5697 if inserted > 0 || node_count > 0 {
5698 self.revision = self.revision.saturating_add(
5699 u64::try_from(inserted.saturating_add(node_count)).unwrap_or(u64::MAX),
5700 );
5701 self.record_decision(
5702 "extend_fresh_int_prefix_keyed_edges_unrecorded",
5703 0.0,
5704 false,
5705 vec![EvidenceTerm {
5706 signal: "batch_edge_count".into(),
5707 observed_value: count_evidence(inserted),
5708 log_likelihood_ratio: -1.0,
5709 }],
5710 );
5711 }
5712 debug_assert_eq!(fresh_slab.edge_count, self.edge_count);
5713 self.storage = fresh_slab;
5714 inserted
5715 }
5716
5717 #[must_use]
5723 pub fn extend_fresh_index_keyed_edges_with_attrs_unrecorded<I, N>(
5724 &mut self,
5725 nodes: N,
5726 edges: I,
5727 ) -> usize
5728 where
5729 I: IntoIterator<Item = (usize, usize, usize, AttrMap)>,
5730 N: IntoIterator<Item = String>,
5731 {
5732 if self.node_count() != 0 || self.edge_count() != 0 {
5733 return 0;
5734 }
5735
5736 let node_labels: Vec<String> = nodes.into_iter().collect();
5737 let mut fresh_slab = {
5738 use multigraph_storage::MgSlabStorage;
5739 let mut slab = MgSlabStorage::new();
5740 slab.node_order.reserve(node_labels.len());
5741 slab.node_attrs.reserve(node_labels.len());
5742 slab.rows.reserve(node_labels.len());
5743 for name in &node_labels {
5744 slab.node_order.insert(name.clone());
5745 slab.node_attrs.push(AttrMap::new());
5746 slab.rows.push(crate::FxIndexMap::default());
5747 }
5748 slab
5749 };
5750
5751 let node_count = node_labels.len();
5752 let mut inserted = 0usize;
5753 for (left_idx, right_idx, key, attrs) in edges {
5754 if node_labels.get(left_idx).is_none() {
5755 continue;
5756 }
5757 if node_labels.get(right_idx).is_none() {
5758 continue;
5759 }
5760 {
5761 use multigraph_storage::{CompactBucket, CompactKeys};
5762 let pair = (left_idx.min(right_idx), left_idx.max(right_idx));
5763 let is_new = match fresh_slab.edges.entry(pair) {
5764 indexmap::map::Entry::Occupied(mut bucket) => {
5765 bucket.get_mut().merge(key, attrs)
5766 }
5767 indexmap::map::Entry::Vacant(slot) => {
5768 slot.insert(CompactBucket::One(key, attrs));
5769 true
5770 }
5771 };
5772 if is_new {
5773 fresh_slab.edge_count += 1;
5774 inserted += 1;
5775 }
5776 match fresh_slab.rows[left_idx].entry(right_idx) {
5777 indexmap::map::Entry::Occupied(mut cell) => {
5778 let _ = cell.get_mut().insert(key);
5779 }
5780 indexmap::map::Entry::Vacant(slot) => {
5781 slot.insert(CompactKeys::One(key));
5782 }
5783 }
5784 if left_idx != right_idx {
5785 match fresh_slab.rows[right_idx].entry(left_idx) {
5786 indexmap::map::Entry::Occupied(mut cell) => {
5787 let _ = cell.get_mut().insert(key);
5788 }
5789 indexmap::map::Entry::Vacant(slot) => {
5790 slot.insert(CompactKeys::One(key));
5791 }
5792 }
5793 }
5794 }
5795 }
5796
5797 self.edge_count = fresh_slab.edge_count;
5798 if node_count > 0 || inserted > 0 {
5799 self.revision = self.revision.saturating_add(
5800 u64::try_from(node_count.saturating_add(inserted)).unwrap_or(u64::MAX),
5801 );
5802 self.record_decision(
5803 "extend_fresh_index_keyed_edges_with_attrs_unrecorded",
5804 0.0,
5805 false,
5806 vec![
5807 EvidenceTerm {
5808 signal: "batch_node_count".into(),
5809 observed_value: count_evidence(node_count),
5810 log_likelihood_ratio: -1.0,
5811 },
5812 EvidenceTerm {
5813 signal: "batch_edge_count".into(),
5814 observed_value: count_evidence(inserted),
5815 log_likelihood_ratio: -1.0,
5816 },
5817 ],
5818 );
5819 }
5820 debug_assert_eq!(fresh_slab.edge_count, self.edge_count);
5821 self.storage = fresh_slab;
5822 inserted
5823 }
5824
5825 fn add_edge_impl(
5826 &mut self,
5827 left: impl Into<String>,
5828 right: impl Into<String>,
5829 explicit_key: Option<usize>,
5830 attrs: AttrMap,
5831 ) -> Result<usize, GraphError> {
5832 let left = left.into();
5833 let right = right.into();
5834 let prev_revision = self.revision;
5837
5838 let unknown_feature = attrs
5839 .keys()
5840 .any(|key| key.starts_with("__fnx_incompatible"));
5841 let incompatibility_probability = if unknown_feature {
5842 1.0
5843 } else if left == right {
5844 0.22
5845 } else {
5846 0.08
5847 };
5848
5849 let action = self
5857 .runtime_policy
5858 .action_for(incompatibility_probability, unknown_feature);
5859
5860 if action == DecisionAction::FailClosed || action == DecisionAction::FullValidate {
5861 self.record_decision(
5862 "add_edge",
5863 incompatibility_probability,
5864 unknown_feature,
5865 vec![EvidenceTerm {
5866 signal: "unknown_incompatible_feature".into(),
5867 observed_value: bool_evidence(unknown_feature),
5868 log_likelihood_ratio: 12.0,
5869 }],
5870 );
5871 return Err(GraphError::FailClosed {
5872 operation: "add_edge",
5873 reason: "incompatible edge metadata".to_owned(),
5874 });
5875 }
5876
5877 let mut left_autocreated = false;
5880 if !self.has_node(&left) {
5881 let _ = self.add_node_with_attrs_unrecorded(left.clone(), AttrMap::new());
5882 left_autocreated = true;
5883 }
5884 let mut right_autocreated = false;
5885 if left == right {
5886 right_autocreated = left_autocreated;
5887 } else if !self.has_node(&right) {
5888 let _ = self.add_node_with_attrs_unrecorded(right.clone(), AttrMap::new());
5889 right_autocreated = true;
5890 }
5891
5892 let left_slot = self
5899 .slab_store()
5900 .node_slot(&left)
5901 .expect("autocreated above");
5902 let right_slot = if left == right {
5903 left_slot
5904 } else {
5905 self.slab_store()
5906 .node_slot(&right)
5907 .expect("autocreated above")
5908 };
5909
5910 let key = explicit_key.unwrap_or_else(|| {
5911 self.slab_store()
5912 .next_edge_key_by_pair(left_slot, right_slot)
5913 });
5914 let pair_was_present = self.slab_store().has_edge_by_pair(left_slot, right_slot);
5915 let changed = self
5916 .slab_store()
5917 .edge_attrs_by_pair(left_slot, right_slot, key)
5918 .is_none_or(|edge_attrs| {
5919 attrs
5920 .iter()
5921 .any(|(attr_key, value)| edge_attrs.get(attr_key) != Some(value))
5922 });
5923 let inserted = self
5924 .slab_store_mut()
5925 .add_keyed_edge_by_pair(left_slot, right_slot, key, attrs);
5926 if inserted {
5927 self.edge_count += 1;
5928 }
5929 let edge_attr_count = self
5930 .slab_store()
5931 .edge_attr_count_by_pair(left_slot, right_slot);
5932 if changed {
5933 self.revision = self.revision.saturating_add(1);
5934 }
5935 if self.revision != prev_revision {
5936 self.advance_int_adj_memo(
5937 prev_revision,
5938 IntAdjEdgeDelta::Add {
5939 left: &left,
5940 right: &right,
5941 left_new: left_autocreated,
5942 right_new: right_autocreated,
5943 pair_new: !pair_was_present,
5944 },
5945 );
5946 }
5947
5948 self.record_decision(
5949 "add_edge",
5950 incompatibility_probability,
5951 unknown_feature,
5952 vec![
5953 EvidenceTerm {
5954 signal: "edge_key".into(),
5955 observed_value: key.to_string().into(),
5956 log_likelihood_ratio: -2.0,
5957 },
5958 EvidenceTerm {
5959 signal: "edge_attr_count".into(),
5960 observed_value: count_evidence(edge_attr_count),
5961 log_likelihood_ratio: -2.0,
5962 },
5963 EvidenceTerm {
5964 signal: "left_autocreated".into(),
5965 observed_value: bool_evidence(left_autocreated),
5966 log_likelihood_ratio: -1.25,
5967 },
5968 EvidenceTerm {
5969 signal: "right_autocreated".into(),
5970 observed_value: bool_evidence(right_autocreated),
5971 log_likelihood_ratio: -1.25,
5972 },
5973 ],
5974 );
5975
5976 Ok(key)
5977 }
5978
5979 pub fn replace_edge_attrs(
5982 &mut self,
5983 left: &str,
5984 right: &str,
5985 key: usize,
5986 attrs: AttrMap,
5987 ) -> bool {
5988 let Some(existing) = self.slab_store().edge_attrs(left, right, key) else {
5989 return false;
5990 };
5991 let changed = *existing != attrs;
5992 if changed {
5993 let replaced = self
5994 .slab_store_mut()
5995 .replace_edge_attrs(left, right, key, attrs);
5996 debug_assert!(replaced, "slab edge disappeared during replacement");
5997 self.revision = self.revision.saturating_add(1);
5998 }
5999 true
6000 }
6001
6002 pub fn replace_node_attrs(&mut self, node: &str, attrs: AttrMap) -> bool {
6004 let Some(existing) = self.slab_store().node_attrs_for(node) else {
6005 return false;
6006 };
6007 let changed = *existing != attrs;
6008 if changed {
6009 self.slab_store_mut().replace_node_attrs(node, attrs);
6010 self.revision = self.revision.saturating_add(1);
6011 }
6012 true
6013 }
6014
6015 pub fn remove_edge(&mut self, left: &str, right: &str, key: Option<usize>) -> bool {
6016 let prev_revision = self.revision;
6019 let removal_key = key.or_else(|| {
6020 self.slab_store()
6021 .edge_keys_iter(left, right)
6022 .and_then(|keys| keys.last().copied())
6023 });
6024
6025 let Some(removal_key) = removal_key else {
6026 return false;
6027 };
6028
6029 if !self
6030 .slab_store_mut()
6031 .remove_edge(left, right, Some(removal_key))
6032 {
6033 return false;
6034 }
6035 self.edge_count -= 1;
6036 let pair_gone = !self.slab_store().has_edge(left, right);
6037 self.revision = self.revision.saturating_add(1);
6038 self.advance_int_adj_memo(
6039 prev_revision,
6040 IntAdjEdgeDelta::Remove {
6041 left,
6042 right,
6043 pair_gone,
6044 },
6045 );
6046 true
6047 }
6048
6049 pub fn clear_edges(&mut self) {
6050 if self.edge_count == 0 {
6051 return;
6052 }
6053
6054 self.slab_store_mut().clear_edges();
6055 self.edge_count = 0;
6056 self.revision = self.revision.saturating_add(1);
6057 }
6058
6059 pub fn remove_node(&mut self, node: &str) -> bool {
6060 if !self.has_node(node) {
6061 return false;
6062 }
6063
6064 let removed = self.slab_store_mut().remove_node(node);
6065 debug_assert!(removed, "node disappeared during remove_node");
6066 self.edge_count = self.slab_store().edge_count;
6067 self.revision = self.revision.saturating_add(1);
6068 true
6069 }
6070
6071 pub fn remove_nodes_from<'a, I>(&mut self, nodes: I) -> (usize, usize)
6077 where
6078 I: IntoIterator<Item = &'a str>,
6079 {
6080 let (removed_nodes, removed_edges) = self.slab_store_mut().remove_nodes_from(nodes);
6081 if removed_nodes == 0 {
6082 return (0, 0);
6083 }
6084 self.edge_count = self.slab_store().edge_count;
6085 self.revision = self
6086 .revision
6087 .saturating_add(u64::try_from(removed_nodes).unwrap_or(u64::MAX));
6088 (removed_nodes, removed_edges)
6089 }
6090
6091 #[must_use]
6092 pub fn edges_ordered(&self) -> Vec<MultiEdgeSnapshot> {
6093 self.slab_store()
6094 .edges_ordered_borrowed()
6095 .into_iter()
6096 .map(|(left, right, key, attrs)| MultiEdgeSnapshot {
6097 left: left.to_owned(),
6098 right: right.to_owned(),
6099 key,
6100 attrs: attrs.clone(),
6101 })
6102 .collect()
6103 }
6104
6105 #[must_use]
6106 pub fn edges_ordered_borrowed(&self) -> Vec<(&str, &str, usize, &AttrMap)> {
6107 self.slab_store().edges_ordered_borrowed()
6108 }
6109
6110 #[must_use]
6117 pub fn weighted_size_int(&self, weight: &str) -> Option<i128> {
6118 self.slab_store().weighted_size_int(weight)
6119 }
6120
6121 #[must_use]
6122 pub fn edges_ordered_indices_borrowed(&self) -> Vec<(usize, usize, usize, &AttrMap)> {
6123 self.slab_store().edges_ordered_indices_borrowed()
6124 }
6125
6126 #[must_use]
6127 pub fn snapshot(&self) -> MultiGraphSnapshot {
6128 self.slab_store().snapshot(self.mode)
6129 }
6130
6131 fn record_decision(
6132 &mut self,
6133 operation: &'static str,
6134 incompatibility_probability: f64,
6135 unknown_incompatible_feature: bool,
6136 evidence: Vec<EvidenceTerm>,
6137 ) -> DecisionAction {
6138 let action = self
6139 .runtime_policy
6140 .action_for(incompatibility_probability, unknown_incompatible_feature);
6141 self.runtime_policy.record(
6142 operation,
6143 action,
6144 incompatibility_probability,
6145 "argmin expected loss over {allow,full_validate,fail_closed}",
6146 evidence,
6147 );
6148 action
6149 }
6150}
6151
6152#[cfg(test)]
6153mod tests {
6154 #[test]
6159 fn compact_key_cells_keep_insertion_order() {
6160 use super::multigraph_storage::{CompactBucket, CompactKeys};
6161
6162 let inserted = [7_usize, 2, 9, 0, 5, 11, 3];
6163 let mut keys = CompactKeys::One(inserted[0]);
6164 let mut bucket = CompactBucket::One(inserted[0], AttrMap::new());
6165 for &key in &inserted[1..] {
6166 let mut attrs = AttrMap::new();
6167 attrs.insert(
6168 "k".to_owned(),
6169 CgseValue::Int(i64::try_from(key).expect("small key")),
6170 );
6171 assert!(keys.insert(key));
6172 assert!(bucket.merge(key, attrs));
6173 }
6174 assert_eq!(keys.order(), inserted);
6175 assert_eq!(bucket.key_order(), inserted);
6176
6177 let mut more = AttrMap::new();
6179 more.insert("extra".to_owned(), CgseValue::Bool(true));
6180 assert!(!keys.insert(9));
6181 assert!(!bucket.merge(9, more));
6182 assert_eq!(keys.order(), inserted);
6183 assert_eq!(bucket.key_order(), inserted);
6184 let merged = bucket.attrs_for(9).expect("key 9 is held");
6185 assert_eq!(merged.get("k"), Some(&CgseValue::Int(9)));
6186 assert_eq!(merged.get("extra"), Some(&CgseValue::Bool(true)));
6187
6188 assert_eq!(keys.shift_remove(0), (true, false));
6190 assert_eq!(bucket.shift_remove(0), (true, false));
6191 assert!(keys.insert(0));
6192 assert!(bucket.merge(0, AttrMap::new()));
6193 assert_eq!(keys.order(), [7, 2, 9, 5, 11, 3, 0]);
6194 assert_eq!(bucket.key_order(), [7, 2, 9, 5, 11, 3, 0]);
6195 }
6196
6197 #[test]
6208 fn multigraph_has_edge_by_indices_matches_the_string_path_across_removals() {
6209 let mut g = MultiGraph::new(CompatibilityMode::Strict);
6210 let names: Vec<String> = (0..8).map(|i| format!("n{i}")).collect();
6211 for name in &names {
6212 g.add_node(name.as_str());
6213 }
6214 for (a, b) in [(0, 1), (2, 3), (3, 5), (5, 7), (0, 7), (4, 4)] {
6215 g.add_edge(names[a].as_str(), names[b].as_str()).unwrap();
6216 }
6217
6218 for step in 0..4 {
6221 if step == 1 {
6222 g.remove_node("n0");
6223 }
6224 if step == 2 {
6225 g.remove_node("n4");
6226 }
6227 if step == 3 {
6228 g.add_node("n8");
6229 g.add_edge("n8", "n3").unwrap();
6230 }
6231 let live: Vec<String> = g.nodes_ordered().iter().map(|s| (*s).to_string()).collect();
6232 for (li, left) in live.iter().enumerate() {
6233 for (ri, right) in live.iter().enumerate() {
6234 let by_index = g.has_edge_by_indices(li, ri);
6235 let by_name = g.has_edge(left.as_str(), right.as_str());
6236 assert_eq!(
6237 by_index, by_name,
6238 "step {step}: has_edge_by_indices({li}, {ri}) disagreed with \
6239 has_edge({left:?}, {right:?})"
6240 );
6241 }
6242 }
6243 let n = live.len();
6245 assert!(!g.has_edge_by_indices(n, 0));
6246 assert!(!g.has_edge_by_indices(0, n + 50));
6247 }
6248 }
6249
6250 #[test]
6273 fn multigraph_node_slot_and_position_are_distinct_index_spaces() {
6274 let mut g = MultiGraph::new(CompatibilityMode::Strict);
6275 for name in ["n0", "n1", "n2", "n3"] {
6276 g.add_node(name);
6277 }
6278 g.add_edge("n2", "n3").unwrap();
6279 assert!(g.has_edge("n2", "n3"));
6280
6281 let pos_before = (g.get_node_index("n2"), g.get_node_index("n3"));
6283 let slot_before = (
6284 g.slab_store().node_slot("n2"),
6285 g.slab_store().node_slot("n3"),
6286 );
6287 assert_eq!(
6288 pos_before, slot_before,
6289 "on a graph with no removals the position and the slot coincide — \
6290 this is why confusing them survives fresh-graph testing"
6291 );
6292
6293 g.remove_node("n0");
6294
6295 let (Some(p2), Some(p3)) = (g.get_node_index("n2"), g.get_node_index("n3")) else {
6296 panic!("nodes vanished after removing an unrelated node");
6297 };
6298 let (Some(s2), Some(s3)) = (
6299 g.slab_store().node_slot("n2"),
6300 g.slab_store().node_slot("n3"),
6301 ) else {
6302 panic!("slots vanished after removing an unrelated node");
6303 };
6304
6305 assert!(
6307 g.has_edge("n2", "n3"),
6308 "removing an unrelated node must not remove this edge"
6309 );
6310
6311 assert_ne!(
6313 (p2, p3),
6314 (s2, s3),
6315 "after a removal the position and slot spaces must diverge; if this \
6316 ever holds, re-derive whether the fast path below is now safe"
6317 );
6318
6319 assert!(
6321 !g.slab_store().has_edge_by_pair(p2, p3),
6322 "position indices happened to answer correctly here — that is luck, \
6323 not a contract; do not take it as licence to feed get_node_index() \
6324 into has_edge_by_pair()"
6325 );
6326
6327 assert!(
6329 g.slab_store().has_edge_by_pair(s2, s3),
6330 "slot indices must agree with the string lookup"
6331 );
6332 }
6333
6334 #[test]
6355 fn multigraph_node_index_matches_int_is_position_keyed_not_slot_keyed() {
6356 let mut g = MultiGraph::new(CompatibilityMode::Strict);
6357 for name in ["0", "1", "2", "3"] {
6358 g.add_node(name);
6359 }
6360
6361 for idx in 0..4 {
6363 assert!(
6364 g.node_index_matches_int(idx),
6365 "on a fresh graph int {idx} sits at position {idx}"
6366 );
6367 assert_eq!(g.get_node_index(&idx.to_string()), Some(idx));
6368 assert_eq!(g.slab_store().node_slot(&idx.to_string()), Some(idx));
6369 }
6370
6371 g.remove_node("0");
6372
6373 let slot_3 = g.slab_store().node_slot("3").expect("node 3 survives");
6375 let pos_3 = g.get_node_index("3").expect("node 3 survives");
6376 assert_eq!(slot_3, 3, "slots are stable across removals");
6377 assert_eq!(pos_3, 2, "positions compact after a removal");
6378 assert_ne!(slot_3, pos_3, "the two spaces have diverged");
6379
6380 assert!(
6384 !g.node_index_matches_int(slot_3),
6385 "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"
6386 );
6387
6388 for idx in 0..3 {
6391 assert!(
6392 !g.node_index_matches_int(idx),
6393 "after the removal no name equals its position, so int {idx} must not report as sitting at position {idx}"
6394 );
6395 assert!(
6396 g.has_node(&(idx + 1).to_string()),
6397 "...while the node itself is still very much present"
6398 );
6399 }
6400 }
6401
6402 use super::{AttrMap, Graph, GraphError, MultiGraph};
6403 use fnx_runtime::{CgseValue, CompatibilityMode, DecisionAction, DecisionRecord};
6404 use proptest::prelude::*;
6405 use std::collections::BTreeSet;
6406
6407 #[test]
6412 fn add_edge_newedge_no_duplicate_adjacency() {
6413 let mut g = Graph::strict();
6414 let _ = g.add_edge("h", "a");
6415 let _ = g.add_edge("h", "b");
6416 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();
6421 for i in 0..n {
6422 if let Some(row) = g.neighbors_indices(i) {
6423 let uniq: BTreeSet<usize> = row.iter().copied().collect();
6424 assert_eq!(
6425 uniq.len(),
6426 row.len(),
6427 "node {i} has duplicate adjacency entries"
6428 );
6429 }
6430 }
6431 assert_eq!(
6433 g.edge_count(),
6434 3,
6435 "duplicate/reversed adds must not create new edges"
6436 );
6437 }
6438
6439 #[test]
6447 #[ignore = "measurement; run with --release --ignored --nocapture"]
6448 fn add_edge_newedge_ab() {
6449 use std::hint::black_box;
6450 use std::time::Instant;
6451
6452 let n = 40000usize;
6453 let time = |use_new: bool| -> f64 {
6454 let t0 = Instant::now();
6455 let mut row: Vec<usize> = Vec::with_capacity(n);
6456 for i in 0..n {
6457 if use_new || !row.contains(&i) {
6458 row.push(i);
6459 }
6460 }
6461 black_box(&row);
6462 t0.elapsed().as_secs_f64()
6463 };
6464 for _ in 0..2 {
6465 black_box(time(true));
6466 black_box(time(false));
6467 }
6468 let median = |v: &[f64]| {
6469 let mut s = v.to_vec();
6470 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6471 s[s.len() / 2]
6472 };
6473 let rounds = 41usize;
6474 let paired = |cand: bool, base: bool| -> Vec<f64> {
6475 let mut v = Vec::with_capacity(rounds);
6476 for r in 0..rounds {
6477 let (tb, tc) = if r % 2 == 0 {
6478 let b = time(base);
6479 let c = time(cand);
6480 (b, c)
6481 } else {
6482 let c = time(cand);
6483 let b = time(base);
6484 (b, c)
6485 };
6486 v.push(tb / tc);
6487 }
6488 v
6489 };
6490 let report = |name: &str, ratios: &[f64]| {
6491 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
6492 let mut sorted = ratios.to_vec();
6493 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
6494 println!(
6495 "ADDEDGE_AB {name}: median={:.4}x win_rate={wins}/{rounds} p5_p95=[{:.4},{:.4}]",
6496 median(ratios),
6497 sorted[rounds * 5 / 100],
6498 sorted[rounds * 95 / 100],
6499 );
6500 };
6501 println!("ADDEDGE_AB adj-row push n={n} rounds={rounds} (>1 = new_edge-flag faster)");
6502 report("NEWEDGE_vs_contains", &paired(true, false));
6503 report("NULL_new_vs_new", &paired(true, true));
6504 }
6505
6506 #[test]
6512 #[ignore = "measurement; run with --release --ignored --nocapture"]
6513 fn degree_by_index_selfloop_ab() {
6514 use std::hint::black_box;
6515 use std::time::Instant;
6516
6517 let n = 20000usize;
6518 let deg = 20usize;
6519 let mut g = Graph::strict();
6520 for i in 0..n {
6521 let _ = g.add_node(format!("n{i}"));
6522 }
6523 for i in 0..n {
6524 for step in 1..=deg {
6525 let _ = g.add_edge(format!("n{i}"), format!("n{}", (i + step) % n));
6526 }
6527 }
6528 for i in (0..n).step_by(1000) {
6529 let _ = g.add_edge(format!("n{i}"), format!("n{i}"));
6530 }
6531
6532 for idx in 0..g.node_count() {
6534 let new = g.degree_by_index(idx);
6535 let old = g.adj_indices[idx].len() + usize::from(g.adj_indices[idx].contains(&idx));
6536 assert_eq!(new, old, "degree_by_index parity vs row.contains at {idx}");
6537 assert_eq!(
6538 new,
6539 g.degree(&format!("n{idx}")),
6540 "degree_by_index parity vs &str degree at {idx}"
6541 );
6542 }
6543
6544 let time = |use_new: bool| -> f64 {
6545 let t0 = Instant::now();
6546 let mut sum = 0usize;
6547 for idx in 0..g.node_count() {
6548 sum += if use_new {
6549 g.degree_by_index(idx)
6550 } else {
6551 g.adj_indices[idx].len() + usize::from(g.adj_indices[idx].contains(&idx))
6552 };
6553 }
6554 black_box(sum);
6555 t0.elapsed().as_secs_f64()
6556 };
6557 for _ in 0..3 {
6558 black_box(time(true));
6559 black_box(time(false));
6560 }
6561 let median = |v: &[f64]| {
6562 let mut s = v.to_vec();
6563 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6564 s[s.len() / 2]
6565 };
6566 let rounds = 41usize;
6567 let paired = |cand: bool, base: bool| -> Vec<f64> {
6568 let mut v = Vec::with_capacity(rounds);
6569 for r in 0..rounds {
6570 let (tb, tc) = if r % 2 == 0 {
6571 let b = time(base);
6572 let c = time(cand);
6573 (b, c)
6574 } else {
6575 let c = time(cand);
6576 let b = time(base);
6577 (b, c)
6578 };
6579 v.push(tb / tc);
6580 }
6581 v
6582 };
6583 let report = |name: &str, ratios: &[f64]| {
6584 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
6585 let mut sorted = ratios.to_vec();
6586 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
6587 println!(
6588 "DEGIDX_AB {name}: median={:.4}x win_rate={wins}/{rounds} p5_p95=[{:.4},{:.4}]",
6589 median(ratios),
6590 sorted[rounds * 5 / 100],
6591 sorted[rounds * 95 / 100],
6592 );
6593 };
6594 println!(
6595 "DEGIDX_AB degree histogram n={n} deg={deg} rounds={rounds} (>1 = O(1) has_edge faster)"
6596 );
6597 report("HASEDGE_vs_contains", &paired(true, false));
6598 report("NULL_new_vs_new", &paired(true, true));
6599 }
6600
6601 #[test]
6611 #[ignore = "measurement; run with --release --ignored --nocapture"]
6612 fn iso_edge_count_ab() {
6613 use std::hint::black_box;
6614 use std::time::Instant;
6615
6616 let n = 4000usize;
6617 let deg = 25usize;
6618 let build = |steps: usize| -> Graph {
6619 let mut g = Graph::strict();
6620 for i in 0..n {
6621 let _ = g.add_node(format!("n{i}"));
6622 }
6623 for i in 0..n {
6624 for step in 1..=steps {
6625 let _ = g.add_edge(format!("n{i}"), format!("n{}", (i + step) % n));
6626 }
6627 }
6628 g
6629 };
6630 let g1 = build(deg);
6632 let g2 = build(deg - 1);
6633
6634 assert_eq!(
6636 g1.edge_count(),
6637 g1.edges_ordered().len(),
6638 "edge_count parity g1"
6639 );
6640 assert_eq!(
6641 g2.edge_count(),
6642 g2.edges_ordered().len(),
6643 "edge_count parity g2"
6644 );
6645 assert_ne!(
6646 g1.edge_count(),
6647 g2.edge_count(),
6648 "workload must exercise the differing-count early-out"
6649 );
6650
6651 let time = |use_new: bool| -> f64 {
6652 let t0 = Instant::now();
6653 let mut acc = 0usize;
6654 for _ in 0..8 {
6655 let differ = if use_new {
6656 g1.edge_count() != g2.edge_count()
6657 } else {
6658 g1.edges_ordered().len() != g2.edges_ordered().len()
6659 };
6660 acc += usize::from(differ);
6661 }
6662 black_box(acc);
6663 t0.elapsed().as_secs_f64()
6664 };
6665 for _ in 0..3 {
6666 black_box(time(true));
6667 black_box(time(false));
6668 }
6669 let median = |v: &[f64]| {
6670 let mut s = v.to_vec();
6671 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6672 s[s.len() / 2]
6673 };
6674 let rounds = 41usize;
6675 let paired = |cand: bool, base: bool| -> Vec<f64> {
6676 let mut v = Vec::with_capacity(rounds);
6677 for r in 0..rounds {
6678 let (tb, tc) = if r % 2 == 0 {
6679 let b = time(base);
6680 let c = time(cand);
6681 (b, c)
6682 } else {
6683 let c = time(cand);
6684 let b = time(base);
6685 (b, c)
6686 };
6687 v.push(tb / tc);
6688 }
6689 v
6690 };
6691 let report = |name: &str, ratios: &[f64]| {
6692 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
6693 let mut sorted = ratios.to_vec();
6694 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
6695 println!(
6696 "ISOEDGE_AB {name}: median={:.4}x win_rate={wins}/{rounds} p5_p95=[{:.4},{:.4}]",
6697 median(ratios),
6698 sorted[rounds * 5 / 100],
6699 sorted[rounds * 95 / 100],
6700 );
6701 };
6702 println!(
6703 "ISOEDGE_AB same-node/different-edge early-out n={n} deg={deg} rounds={rounds} (>1 = edge_count faster)"
6704 );
6705 report("EDGECOUNT_vs_edgesordered", &paired(true, false));
6706 report("NULL_new_vs_new", &paired(true, true));
6707 }
6708
6709 #[test]
6717 #[ignore = "measurement; run with --release --ignored --nocapture"]
6718 fn nodes_selfloop_idx_ab() {
6719 use std::hint::black_box;
6720 use std::time::Instant;
6721
6722 let n = 40_000usize;
6724 let deg = 10usize;
6725 let mut g = Graph::strict();
6726 for i in 0..n {
6727 let _ = g.add_node(i.to_string());
6728 }
6729 for i in 0..n {
6730 for step in 1..=deg {
6731 let _ = g.add_edge(i.to_string(), ((i + step) % n).to_string());
6732 }
6733 }
6734
6735 let names = g.nodes_ordered();
6736 let old_scan =
6737 |g: &Graph| -> usize { names.iter().filter(|&&node| g.has_edge(node, node)).count() };
6738 let new_scan = |g: &Graph| -> usize {
6739 (0..names.len())
6740 .filter(|&i| g.has_edge_by_indices(i, i))
6741 .count()
6742 };
6743 assert_eq!(
6744 old_scan(&g),
6745 new_scan(&g),
6746 "nodes_with_selfloops parity (self-loop count)"
6747 );
6748
6749 let time = |cand: bool| -> f64 {
6750 let t0 = Instant::now();
6751 let c = if cand { new_scan(&g) } else { old_scan(&g) };
6752 black_box(c);
6753 t0.elapsed().as_secs_f64()
6754 };
6755 for _ in 0..3 {
6756 black_box(time(true));
6757 black_box(time(false));
6758 }
6759 let median = |v: &[f64]| {
6760 let mut s = v.to_vec();
6761 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6762 s[s.len() / 2]
6763 };
6764 let rounds = 61usize;
6765 let paired = |cand: bool, base: bool| -> Vec<f64> {
6766 let mut v = Vec::with_capacity(rounds);
6767 for r in 0..rounds {
6768 let (tb, tc) = if r % 2 == 0 {
6769 let b = time(base);
6770 let c = time(cand);
6771 (b, c)
6772 } else {
6773 let c = time(cand);
6774 let b = time(base);
6775 (b, c)
6776 };
6777 v.push(tb / tc);
6778 }
6779 v
6780 };
6781 let report = |name: &str, ratios: &[f64]| {
6782 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
6783 let mut sorted = ratios.to_vec();
6784 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
6785 println!(
6786 "SELFLOOPIDX_AB {name}: median={:.4}x win_rate={wins}/{rounds} p5_p95=[{:.4},{:.4}]",
6787 median(ratios),
6788 sorted[rounds * 5 / 100],
6789 sorted[rounds * 95 / 100],
6790 );
6791 };
6792 println!(
6793 "SELFLOOPIDX_AB nodes_with_selfloops n={n} deg={deg} rounds={rounds} (>1 = index probe faster)"
6794 );
6795 report("HASEDGEIDX_vs_hasedge", &paired(true, false));
6796 report("NULL_new_vs_new", &paired(true, true));
6797 }
6798
6799 #[test]
6807 #[ignore = "measurement; run with --release --ignored --nocapture"]
6808 fn number_of_selfloops_idx_ab() {
6809 use std::hint::black_box;
6810 use std::time::Instant;
6811
6812 let n = 20_000usize;
6815 let deg = 20usize;
6816 let mut g = Graph::strict();
6817 for i in 0..n {
6818 let _ = g.add_node(i.to_string());
6819 }
6820 for i in 0..n {
6821 for step in 1..=deg {
6822 let _ = g.add_edge(i.to_string(), ((i + step) % n).to_string());
6823 }
6824 }
6825
6826 let old_count = |g: &Graph| -> usize {
6827 g.edges_ordered()
6828 .iter()
6829 .filter(|e| e.left == e.right)
6830 .count()
6831 };
6832 let new_count = |g: &Graph| -> usize {
6833 (0..g.node_count())
6834 .filter(|&i| g.has_edge_by_indices(i, i))
6835 .count()
6836 };
6837 assert_eq!(old_count(&g), new_count(&g), "number_of_selfloops parity");
6838
6839 let time = |cand: bool| -> f64 {
6840 let t0 = Instant::now();
6841 let c = if cand { new_count(&g) } else { old_count(&g) };
6842 black_box(c);
6843 t0.elapsed().as_secs_f64()
6844 };
6845 for _ in 0..3 {
6846 black_box(time(true));
6847 black_box(time(false));
6848 }
6849 let median = |v: &[f64]| {
6850 let mut s = v.to_vec();
6851 s.sort_by(|a, b| a.partial_cmp(b).unwrap());
6852 s[s.len() / 2]
6853 };
6854 let rounds = 61usize;
6855 let paired = |cand: bool, base: bool| -> Vec<f64> {
6856 let mut v = Vec::with_capacity(rounds);
6857 for r in 0..rounds {
6858 let (tb, tc) = if r % 2 == 0 {
6859 let b = time(base);
6860 let c = time(cand);
6861 (b, c)
6862 } else {
6863 let c = time(cand);
6864 let b = time(base);
6865 (b, c)
6866 };
6867 v.push(tb / tc);
6868 }
6869 v
6870 };
6871 let report = |name: &str, ratios: &[f64]| {
6872 let wins = ratios.iter().filter(|&&r| r > 1.0).count();
6873 let mut sorted = ratios.to_vec();
6874 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
6875 println!(
6876 "NUMSELFIDX_AB {name}: median={:.4}x win_rate={wins}/{rounds} p5_p95=[{:.4},{:.4}]",
6877 median(ratios),
6878 sorted[rounds * 5 / 100],
6879 sorted[rounds * 95 / 100],
6880 );
6881 };
6882 println!(
6883 "NUMSELFIDX_AB number_of_selfloops n={n} deg={deg} rounds={rounds} (>1 = index count faster)"
6884 );
6885 report("INDEXCOUNT_vs_edgesordered", &paired(true, false));
6886 report("NULL_new_vs_new", &paired(true, true));
6887 }
6888
6889 #[test]
6894 fn kneser_push_no_duplicate_adjacency() {
6895 let g = Graph::kneser(CompatibilityMode::Strict, 10, 3);
6897 assert_eq!(g.node_count(), 120, "C(10,3) nodes");
6898 assert_eq!(g.edge_count(), 2100, "120*35/2 Kneser edges");
6899 let mut total_deg = 0usize;
6900 for (i, row) in g.adj_indices.iter().enumerate() {
6901 let uniq: BTreeSet<usize> = row.iter().copied().collect();
6902 assert_eq!(
6903 uniq.len(),
6904 row.len(),
6905 "node {i} has duplicate adjacency entries"
6906 );
6907 assert_eq!(row.len(), 35, "Kneser(10,3) is 35-regular");
6908 total_deg += row.len();
6909 }
6910 assert_eq!(total_deg, 2 * 2100, "handshake: sum of degrees == 2|E|");
6911 }
6912
6913 fn node_name(id: u8) -> String {
6914 format!("n{}", id % 8)
6915 }
6916
6917 fn canonical_edge(left: &str, right: &str) -> (String, String) {
6918 if left <= right {
6919 (left.to_owned(), right.to_owned())
6920 } else {
6921 (right.to_owned(), left.to_owned())
6922 }
6923 }
6924
6925 fn assert_int_adjacency_matches(g: &MultiGraph) {
6932 let expected: Vec<Vec<usize>> = g
6933 .nodes_ordered()
6934 .iter()
6935 .map(|name| {
6936 g.neighbors_iter(name).map_or_else(Vec::new, |it| {
6937 it.map(|v| g.get_node_index(v).unwrap()).collect()
6938 })
6939 })
6940 .collect();
6941 g.with_int_adjacency(|adj| {
6942 assert_eq!(
6943 adj,
6944 expected.as_slice(),
6945 "int adjacency must mirror the stable-slot rows exactly"
6946 );
6947 });
6948 }
6949
6950 #[test]
6951 fn thp6w_int_adjacency_invariant_across_mutations() {
6952 let mut g = MultiGraph::strict();
6953 for i in 0..8 {
6954 g.add_node(format!("n{i}"));
6955 }
6956 let _ = g.add_edge("n0", "n1");
6958 let _ = g.add_edge("n0", "n1"); let _ = g.add_edge("n1", "n2");
6960 let _ = g.add_edge("n2", "n3");
6961 let _ = g.add_edge("n3", "n0");
6962 let _ = g.add_edge("n4", "n4"); let _ = g.add_edge("n5", "n6");
6964 assert_int_adjacency_matches(&g);
6966
6967 assert_int_adjacency_matches(&g);
6970 g.add_node("n9");
6971 assert_int_adjacency_matches(&g);
6972 assert_int_adjacency_matches(&g);
6974 let _ = g.add_edge("n9", "n0");
6975 assert_int_adjacency_matches(&g);
6976 assert_int_adjacency_matches(&g);
6978 let _ = g.add_edge("n9", "n0");
6979 assert_int_adjacency_matches(&g);
6980 assert_int_adjacency_matches(&g);
6982 assert!(g.remove_edge("n0", "n1", None));
6983 assert_int_adjacency_matches(&g);
6984 assert_int_adjacency_matches(&g);
6986 assert!(g.remove_edge("n5", "n6", None));
6987 assert_int_adjacency_matches(&g);
6988 assert_int_adjacency_matches(&g);
6990 assert!(g.remove_node("n2"));
6991 assert_int_adjacency_matches(&g);
6992 assert_int_adjacency_matches(&g);
6995 g.reorder_rows_for_nx_copy_walk();
6996 assert_int_adjacency_matches(&g);
6997 assert_int_adjacency_matches(&g);
6999 g.clear_edges();
7000 assert_int_adjacency_matches(&g);
7001 }
7002
7003 #[test]
7004 fn reorder_rows_like_restores_the_source_rows() {
7005 let mut source = Graph::strict();
7007 for (u, v) in [("x", "c"), ("x", "a"), ("x", "b"), ("a", "b"), ("c", "b")] {
7008 let _ = source.add_edge(u, v);
7009 }
7010 let mut copy = Graph::strict();
7014 for node in ["a", "b", "c", "x", "y"] {
7015 let _ = copy.add_node(node);
7016 }
7017 for (u, v) in [
7018 ("a", "x"),
7019 ("a", "b"),
7020 ("b", "x"),
7021 ("b", "c"),
7022 ("c", "x"),
7023 ("x", "y"),
7024 ] {
7025 let _ = copy.add_edge(u, v);
7026 }
7027 assert_eq!(copy.neighbors("b"), Some(vec!["a", "x", "c"]));
7028 let before = copy.revision;
7029 copy.reorder_rows_like(&source);
7030 assert_eq!(copy.neighbors("a"), Some(vec!["x", "b"]));
7031 assert_eq!(copy.neighbors("b"), Some(vec!["x", "a", "c"]));
7032 assert_eq!(copy.neighbors("c"), Some(vec!["x", "b"]));
7033 assert_eq!(copy.neighbors("x"), Some(vec!["c", "a", "b", "y"]));
7035 assert_eq!(copy.neighbors("y"), Some(vec!["x"]));
7036 assert!(copy.revision > before, "a moved row must bump the revision");
7037 let after = copy.revision;
7038 copy.reorder_rows_like(&source);
7039 assert_eq!(copy.revision, after, "an ordered graph is left alone");
7040 assert_eq!(copy.edge_count(), 6);
7041 }
7042
7043 #[test]
7044 fn set_row_orders_takes_permutations_only_and_all_or_nothing() {
7045 let mut g = Graph::strict();
7046 for (u, v) in [("a", "b"), ("a", "c"), ("b", "c")] {
7047 let _ = g.add_edge(u, v);
7048 }
7049 assert!(g.set_row_orders(&[vec![2, 1], vec![2, 0], vec![1, 0]]));
7051 assert_eq!(g.neighbors("a"), Some(vec!["c", "b"]));
7052 assert_eq!(g.neighbors("b"), Some(vec!["c", "a"]));
7053 assert_eq!(g.neighbors("c"), Some(vec!["b", "a"]));
7054 let before = g.revision;
7055 assert!(!g.set_row_orders(&[vec![1, 2], vec![2, 2], vec![0, 1]]));
7058 assert!(!g.set_row_orders(&[vec![1, 2], vec![0, 2]]));
7060 assert!(!g.set_row_orders(&[vec![1], vec![0, 2], vec![0, 1]]));
7061 assert!(!g.set_row_orders(&[vec![1, 7], vec![0, 2], vec![0, 1]]));
7062 assert_eq!(g.neighbors("a"), Some(vec!["c", "b"]));
7063 assert_eq!(g.revision, before);
7064 }
7065
7066 #[test]
7067 fn multigraph_reorder_rows_like_restores_the_source_rows() {
7068 let mut source = MultiGraph::strict();
7070 for (u, v) in [("x", "c"), ("x", "a"), ("x", "b"), ("x", "a")] {
7071 let _ = source.add_edge(u, v);
7072 }
7073 let mut copy = MultiGraph::strict();
7076 for node in ["a", "b", "c", "x", "y"] {
7077 let _ = copy.add_node(node);
7078 }
7079 for (u, v) in [("a", "x"), ("a", "x"), ("b", "x"), ("c", "x"), ("x", "y")] {
7080 let _ = copy.add_edge(u, v);
7081 }
7082 assert_eq!(copy.neighbors("x"), Some(vec!["a", "b", "c", "y"]));
7083 assert_int_adjacency_matches(©); copy.reorder_rows_like(&source);
7085 assert_eq!(copy.neighbors("x"), Some(vec!["c", "a", "b", "y"]));
7088 assert_eq!(copy.neighbors("a"), Some(vec!["x"]));
7089 assert_eq!(copy.edge_keys("x", "a"), Some(vec![0, 1]));
7090 assert_eq!(copy.edge_count(), 5);
7091 assert_int_adjacency_matches(©);
7093 }
7094
7095 fn int_adj_memo_is_warm(g: &MultiGraph) -> bool {
7099 g.int_adj_cache
7100 .0
7101 .read()
7102 .expect("int_adj_cache poisoned")
7103 .as_ref()
7104 .is_some_and(|(rev, _)| *rev == g.revision)
7105 }
7106
7107 #[test]
7113 fn thp6w_s4_single_edge_mutations_advance_warm_memo() {
7114 let mut g = MultiGraph::strict();
7115 let _ = g.add_edge("a", "b");
7116 assert_int_adjacency_matches(&g); let _ = g.add_edge("b", "c");
7120 assert!(
7121 int_adj_memo_is_warm(&g),
7122 "add_edge must advance, not invalidate"
7123 );
7124 assert_int_adjacency_matches(&g);
7125 let _ = g.add_edge("d", "e");
7127 assert!(int_adj_memo_is_warm(&g));
7128 assert_int_adjacency_matches(&g);
7129 let _ = g.add_edge("f", "f");
7131 assert!(int_adj_memo_is_warm(&g));
7132 assert_int_adjacency_matches(&g);
7133 let _ = g.add_edge("a", "b");
7135 assert!(int_adj_memo_is_warm(&g));
7136 assert_int_adjacency_matches(&g);
7137 let mut attrs = AttrMap::new();
7139 attrs.insert("w".to_owned(), fnx_runtime::CgseValue::Int(2));
7140 let _ = g.add_edge_with_key_and_attrs("a", "b", 0, attrs);
7141 assert!(int_adj_memo_is_warm(&g));
7142 assert_int_adjacency_matches(&g);
7143 assert!(g.remove_edge("a", "b", None));
7145 assert!(int_adj_memo_is_warm(&g));
7146 assert_int_adjacency_matches(&g);
7147 assert!(g.remove_edge("a", "b", None));
7149 assert!(int_adj_memo_is_warm(&g));
7150 assert_int_adjacency_matches(&g);
7151 let _ = g.add_edge("b", "x");
7154 let _ = g.add_edge("b", "y");
7155 assert!(g.remove_edge("b", "c", None));
7156 assert!(int_adj_memo_is_warm(&g));
7157 assert_int_adjacency_matches(&g);
7158 assert!(g.remove_edge("f", "f", None));
7160 assert!(int_adj_memo_is_warm(&g));
7161 assert_int_adjacency_matches(&g);
7162 assert!(g.remove_node("x"));
7164 assert!(
7165 !int_adj_memo_is_warm(&g),
7166 "remove_node stays invalidate-only; a warm memo here would serve stale indices"
7167 );
7168 assert_int_adjacency_matches(&g);
7169 }
7170
7171 #[test]
7180 fn thp6w_s5_int_storage_proto_parity_gates() {
7181 use super::multigraph_storage::MgIntStorageProto;
7182
7183 let stream: Vec<(&str, &str, usize)> = vec![
7184 ("2", "10", 0),
7185 ("10", "2", 1), ("1", "1", 0), ("2", "3", 0),
7188 ("3", "2", 0), ("10", "1", 5), ("0", "2", 0), ("2", "10", 1), ("4", "5", 2),
7193 ("5", "4", 0), ("1", "1", 1), ];
7196 let attrs_for = |i: usize| {
7197 let mut attrs = AttrMap::new();
7198 attrs.insert(
7199 format!("a{}", i % 3),
7200 fnx_runtime::CgseValue::Int(i64::try_from(i).unwrap()),
7201 );
7202 attrs
7203 };
7204
7205 let mut mg = MultiGraph::strict();
7206 let _ = mg.extend_keyed_edges_with_attrs_unrecorded(
7207 stream
7208 .iter()
7209 .enumerate()
7210 .map(|(i, (l, r, k))| ((*l).to_owned(), (*r).to_owned(), *k, attrs_for(i))),
7211 );
7212 let mut proto = MgIntStorageProto::new();
7213 for (i, (l, r, k)) in stream.iter().enumerate() {
7214 proto.add_keyed_edge(l, r, *k, attrs_for(i));
7215 }
7216 let mut compact = super::multigraph_storage::MgIntStorageProtoCompact::new();
7218 for (i, (l, r, k)) in stream.iter().enumerate() {
7219 compact.add_keyed_edge(l, r, *k, attrs_for(i));
7220 }
7221
7222 assert_eq!(
7224 proto
7225 .node_names
7226 .iter()
7227 .map(String::as_str)
7228 .collect::<Vec<_>>(),
7229 mg.nodes_ordered(),
7230 "node order must match"
7231 );
7232 assert_eq!(
7233 compact
7234 .node_names
7235 .iter()
7236 .map(String::as_str)
7237 .collect::<Vec<_>>(),
7238 mg.nodes_ordered(),
7239 "compact node order must match"
7240 );
7241 for (i, name) in mg.nodes_ordered().iter().enumerate() {
7243 let mg_row: Vec<&str> = mg
7244 .neighbors_iter(name)
7245 .map_or_else(Vec::new, Iterator::collect);
7246 assert_eq!(proto.neighbor_names(i), mg_row, "row order for node {name}");
7247 assert_eq!(
7248 compact.neighbor_names(i),
7249 mg_row,
7250 "compact row order for node {name}"
7251 );
7252 for v in &mg_row {
7253 assert_eq!(
7254 proto.key_order(name, v),
7255 mg.edge_keys_vec(name, v),
7256 "key order for pair ({name}, {v})"
7257 );
7258 assert_eq!(
7259 compact.key_order(name, v),
7260 mg.edge_keys_vec(name, v),
7261 "compact key order for pair ({name}, {v})"
7262 );
7263 }
7264 }
7265 assert_eq!(proto.edge_count, mg.edge_count(), "edge_count must match");
7267 assert_eq!(
7268 compact.edge_count,
7269 mg.edge_count(),
7270 "compact edge_count must match"
7271 );
7272 let l = *proto.node_index.get("2").expect("proto must retain node 2");
7275 let r = *proto
7276 .node_index
7277 .get("10")
7278 .expect("proto must retain node 10");
7279 let proto_attrs = proto.edges[&(l.min(r), l.max(r))]
7280 .get(&1)
7281 .expect("proto (2,10,1) cell");
7282 assert_eq!(
7283 Some(proto_attrs),
7284 mg.edge_attrs("2", "10", 1),
7285 "merged attrs on the dup (pair, key) cell must match"
7286 );
7287 assert_eq!(
7288 compact.edges[&(l.min(r), l.max(r))].attrs_for(1),
7289 mg.edge_attrs("2", "10", 1),
7290 "compact merged attrs on the dup (pair, key) cell must match"
7291 );
7292 }
7293
7294 fn assert_proto_matches_mg(
7298 mg: &MultiGraph,
7299 proto: &super::multigraph_storage::MgIntStorageProto,
7300 ) {
7301 assert_eq!(
7302 proto
7303 .node_names
7304 .iter()
7305 .map(String::as_str)
7306 .collect::<Vec<_>>(),
7307 mg.nodes_ordered(),
7308 "node order"
7309 );
7310 for (i, name) in proto.node_names.iter().enumerate() {
7311 assert_eq!(
7312 proto.node_index.get(name),
7313 Some(&i),
7314 "node_index desync for {name}"
7315 );
7316 }
7317 for (i, name) in mg.nodes_ordered().iter().enumerate() {
7318 let mg_row: Vec<&str> = mg
7319 .neighbors_iter(name)
7320 .map_or_else(Vec::new, Iterator::collect);
7321 assert_eq!(proto.neighbor_names(i), mg_row, "row order for {name}");
7322 for v in &mg_row {
7323 assert_eq!(
7324 proto.key_order(name, v),
7325 mg.edge_keys_vec(name, v),
7326 "key order for ({name}, {v})"
7327 );
7328 }
7329 }
7330 assert_eq!(proto.edge_count, mg.edge_count(), "edge_count");
7331 }
7332
7333 #[test]
7339 fn thp6w_s7_removal_renumber_parity_gauntlet() {
7340 use super::multigraph_storage::MgIntStorageProto;
7341
7342 let stream: Vec<(&str, &str, usize)> = vec![
7343 ("2", "10", 0),
7344 ("10", "2", 1),
7345 ("1", "1", 0),
7346 ("2", "3", 0),
7347 ("3", "2", 0),
7348 ("10", "1", 5),
7349 ("0", "2", 0),
7350 ("2", "10", 1),
7351 ("4", "5", 2),
7352 ("5", "4", 0),
7353 ("1", "1", 1),
7354 ];
7355 let mut mg = MultiGraph::strict();
7356 let _ = mg.extend_keyed_edges_with_attrs_unrecorded(
7357 stream
7358 .iter()
7359 .map(|(l, r, k)| ((*l).to_owned(), (*r).to_owned(), *k, AttrMap::new())),
7360 );
7361 let mut proto = MgIntStorageProto::new();
7362 for (l, r, k) in &stream {
7363 proto.add_keyed_edge(l, r, *k, AttrMap::new());
7364 }
7365 assert_proto_matches_mg(&mg, &proto);
7366
7367 assert_eq!(
7370 mg.remove_edge("4", "5", None),
7371 proto.remove_edge("4", "5", None)
7372 );
7373 assert_proto_matches_mg(&mg, &proto);
7374 assert_eq!(
7376 mg.remove_edge("2", "10", Some(0)),
7377 proto.remove_edge("2", "10", Some(0))
7378 );
7379 assert_proto_matches_mg(&mg, &proto);
7380 assert_eq!(
7382 mg.remove_edge("2", "10", None),
7383 proto.remove_edge("2", "10", None)
7384 );
7385 assert_proto_matches_mg(&mg, &proto);
7386 assert_eq!(
7388 mg.remove_edge("1", "1", Some(0)),
7389 proto.remove_edge("1", "1", Some(0))
7390 );
7391 assert_proto_matches_mg(&mg, &proto);
7392 assert_eq!(
7394 mg.remove_edge("0", "10", None),
7395 proto.remove_edge("0", "10", None)
7396 );
7397 assert_eq!(
7398 mg.remove_edge("2", "3", Some(9)),
7399 proto.remove_edge("2", "3", Some(9))
7400 );
7401 assert_proto_matches_mg(&mg, &proto);
7402
7403 assert_eq!(mg.remove_node("2"), proto.remove_node("2"));
7405 assert_proto_matches_mg(&mg, &proto);
7406 mg.extend_keyed_edges_with_attrs_unrecorded(vec![
7408 ("3".to_owned(), "99".to_owned(), 0, AttrMap::new()),
7409 ("0".to_owned(), "3".to_owned(), 0, AttrMap::new()),
7410 ]);
7411 proto.add_keyed_edge("3", "99", 0, AttrMap::new());
7412 proto.add_keyed_edge("0", "3", 0, AttrMap::new());
7413 assert_proto_matches_mg(&mg, &proto);
7414 assert_eq!(mg.remove_node("10"), proto.remove_node("10"));
7416 assert_proto_matches_mg(&mg, &proto);
7417 assert_eq!(mg.remove_node("1"), proto.remove_node("1"));
7418 assert_proto_matches_mg(&mg, &proto);
7419 assert_eq!(mg.remove_node("2"), proto.remove_node("2"));
7421 assert_proto_matches_mg(&mg, &proto);
7422 }
7423
7424 #[test]
7435 #[ignore = "paired A/B benchmark; run --release with --ignored --nocapture"]
7436 fn thp6w_s8_removal_cost_ab() {
7437 use super::multigraph_storage::MgIntStorageProto;
7438 use std::collections::HashMap;
7439 use std::hint::black_box;
7440 use std::time::Instant;
7441
7442 fn push_edge(
7443 stream: &mut Vec<(usize, usize, usize)>,
7444 key_counter: &mut HashMap<(usize, usize), usize>,
7445 u: usize,
7446 v: usize,
7447 ) {
7448 let entry = key_counter.entry((u.min(v), u.max(v))).or_insert(0);
7449 stream.push((u, v, *entry));
7450 *entry += 1;
7451 }
7452
7453 let n = 20000usize;
7454 let mut stream = Vec::new();
7455 let mut key_counter = HashMap::new();
7456 for i in 0..n {
7457 push_edge(&mut stream, &mut key_counter, i, (i + 1) % n);
7458 }
7459 let mut state = 0x2545_F491_4F6C_DD1D_u64;
7460 let draw = |state: &mut u64| -> usize {
7461 *state = state
7462 .wrapping_mul(6_364_136_223_846_793_005)
7463 .wrapping_add(1_442_695_040_888_963_407);
7464 usize::try_from(*state >> 33).unwrap() % n
7465 };
7466 for _ in 0..3 * n {
7467 let u = draw(&mut state);
7468 let v = draw(&mut state);
7469 push_edge(&mut stream, &mut key_counter, u, v);
7470 }
7471 let victims: Vec<String> = (0..n).step_by(100).map(|i| i.to_string()).collect();
7472
7473 let time_current_removals = |stream: &[(usize, usize, usize)]| -> f64 {
7474 let mut g = MultiGraph::strict();
7475 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
7476 let t0 = Instant::now();
7477 for name in &victims {
7478 assert!(g.remove_node(name));
7479 }
7480 let dt = t0.elapsed().as_secs_f64();
7481 black_box(g.edge_count());
7482 dt
7483 };
7484 let time_proto_removals = |stream: &[(usize, usize, usize)]| -> f64 {
7485 let mut proto = MgIntStorageProto::bulk_load_int_prefix(n, stream.iter().copied());
7486 let t0 = Instant::now();
7487 for name in &victims {
7488 assert!(proto.remove_node(name));
7489 }
7490 let dt = t0.elapsed().as_secs_f64();
7491 black_box(proto.edge_count);
7492 dt
7493 };
7494
7495 let mut g = MultiGraph::strict();
7497 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
7498 let mut proto = MgIntStorageProto::bulk_load_int_prefix(n, stream.iter().copied());
7499 for name in &victims {
7500 assert!(g.remove_node(name));
7501 assert!(proto.remove_node(name));
7502 }
7503 assert_eq!(
7504 proto.edge_count,
7505 g.edge_count(),
7506 "removal arms diverged in edge_count"
7507 );
7508 assert_eq!(
7509 proto
7510 .node_names
7511 .iter()
7512 .map(String::as_str)
7513 .collect::<Vec<_>>(),
7514 g.nodes_ordered(),
7515 "removal arms diverged in node order"
7516 );
7517
7518 let median = |samples: &mut Vec<f64>| -> f64 {
7519 samples.sort_by(f64::total_cmp);
7520 samples[samples.len() / 2]
7521 };
7522 let (mut cur, mut pro, mut nul) = (Vec::new(), Vec::new(), Vec::new());
7523 for _ in 0..7 {
7524 cur.push(time_current_removals(&stream));
7525 pro.push(time_proto_removals(&stream));
7526 nul.push(time_current_removals(&stream));
7527 }
7528 let time_slab_removals = |stream: &[(usize, usize, usize)]| -> f64 {
7530 let mut slab = super::multigraph_storage::MgSlabStorage::bulk_load_int_prefix(
7531 n,
7532 stream.iter().copied(),
7533 );
7534 let t0 = Instant::now();
7535 for name in &victims {
7536 assert!(slab.remove_node(name));
7537 }
7538 let dt = t0.elapsed().as_secs_f64();
7539 black_box(slab.edge_count);
7540 dt
7541 };
7542 let mut g2 = MultiGraph::strict();
7543 let _ = g2.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
7544 let mut slab = super::multigraph_storage::MgSlabStorage::bulk_load_int_prefix(
7545 n,
7546 stream.iter().copied(),
7547 );
7548 for name in &victims {
7549 assert!(g2.remove_node(name));
7550 assert!(slab.remove_node(name));
7551 }
7552 assert_eq!(
7553 slab.edge_count,
7554 g2.edge_count(),
7555 "slab removal arm diverged in edge_count"
7556 );
7557 assert_eq!(
7558 slab.node_order
7559 .keys()
7560 .map(String::as_str)
7561 .collect::<Vec<_>>(),
7562 g2.nodes_ordered(),
7563 "slab removal arm diverged in node order"
7564 );
7565 let mut sla = Vec::new();
7566 for _ in 0..7 {
7567 sla.push(time_slab_removals(&stream));
7568 }
7569 let msl = median(&mut sla);
7570
7571 let (mc, mp, mn) = (median(&mut cur), median(&mut pro), median(&mut nul));
7572 println!(
7573 "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}",
7574 stream.len(),
7575 victims.len(),
7576 mp / mc,
7577 msl / mc,
7578 mc / mn,
7579 );
7580 }
7581
7582 fn assert_slab_matches_mg(mg: &MultiGraph, slab: &super::multigraph_storage::MgSlabStorage) {
7587 let mg_node_order = mg.nodes_ordered();
7588 assert_eq!(
7589 slab.node_order
7590 .keys()
7591 .map(String::as_str)
7592 .collect::<Vec<_>>(),
7593 mg_node_order,
7594 "slab node order"
7595 );
7596 for (name, slot) in slab.node_order.iter() {
7597 assert_eq!(
7598 slab.node_order.name(slot),
7599 Some(name.as_str()),
7600 "slot names desync for {name}"
7601 );
7602 }
7603 for &free in slab.node_order.free_slots() {
7604 assert!(
7605 slab.node_order.name(free).is_none(),
7606 "free slot {free} still named"
7607 );
7608 assert!(slab.rows[free].is_empty(), "free slot {free} has row cells");
7609 assert!(
7610 slab.node_attrs[free].is_empty(),
7611 "free slot {free} still has node attrs"
7612 );
7613 }
7614 for &name in &mg_node_order {
7615 assert_eq!(
7616 slab.node_attrs_for(name),
7617 mg.node_attrs(name),
7618 "slab node attrs for {name}"
7619 );
7620 }
7621 for (pos, &name) in mg_node_order.iter().enumerate() {
7622 let mg_row = mg.neighbors(name).unwrap_or_default();
7623 assert_eq!(
7624 slab.neighbor_names(pos),
7625 mg_row,
7626 "slab row order for {name}"
7627 );
7628 for &v in &mg_row {
7629 let mg_keys = mg.edge_keys_vec(name, v);
7630 assert_eq!(
7631 slab.key_order(name, v),
7632 mg_keys,
7633 "slab key order for ({name}, {v})"
7634 );
7635 }
7636 }
7637 assert_eq!(slab.edge_count, mg.edge_count, "slab edge_count");
7638 }
7639
7640 #[test]
7647 fn thp6w_s9_slab_recycling_parity_gauntlet() {
7648 use super::multigraph_storage::MgSlabStorage;
7649
7650 let stream: Vec<(&str, &str, usize)> = vec![
7651 ("2", "10", 0),
7652 ("10", "2", 1),
7653 ("1", "1", 0),
7654 ("2", "3", 0),
7655 ("3", "2", 0),
7656 ("10", "1", 5),
7657 ("0", "2", 0),
7658 ("2", "10", 1),
7659 ("4", "5", 2),
7660 ("5", "4", 0),
7661 ("1", "1", 1),
7662 ];
7663 let mut mg = MultiGraph::strict();
7664 let _ = mg.extend_keyed_edges_with_attrs_unrecorded(
7665 stream
7666 .iter()
7667 .map(|(l, r, k)| ((*l).to_owned(), (*r).to_owned(), *k, AttrMap::new())),
7668 );
7669 let mut slab = MgSlabStorage::new();
7670 for (l, r, k) in &stream {
7671 slab.add_keyed_edge(l, r, *k, AttrMap::new());
7672 }
7673 assert_slab_matches_mg(&mg, &slab);
7674
7675 assert_eq!(
7677 mg.remove_edge("4", "5", None),
7678 slab.remove_edge("4", "5", None)
7679 );
7680 assert_slab_matches_mg(&mg, &slab);
7681 assert_eq!(
7682 mg.remove_edge("2", "10", Some(0)),
7683 slab.remove_edge("2", "10", Some(0))
7684 );
7685 assert_slab_matches_mg(&mg, &slab);
7686 assert_eq!(
7687 mg.remove_edge("2", "10", None),
7688 slab.remove_edge("2", "10", None)
7689 );
7690 assert_slab_matches_mg(&mg, &slab);
7691 assert_eq!(
7692 mg.remove_edge("1", "1", Some(0)),
7693 slab.remove_edge("1", "1", Some(0))
7694 );
7695 assert_slab_matches_mg(&mg, &slab);
7696 assert_eq!(
7697 mg.remove_edge("0", "10", None),
7698 slab.remove_edge("0", "10", None)
7699 );
7700 assert_eq!(
7701 mg.remove_edge("2", "3", Some(9)),
7702 slab.remove_edge("2", "3", Some(9))
7703 );
7704 assert_slab_matches_mg(&mg, &slab);
7705
7706 assert_eq!(mg.remove_node("2"), slab.remove_node("2"));
7708 assert_slab_matches_mg(&mg, &slab);
7709 mg.extend_keyed_edges_with_attrs_unrecorded(vec![(
7712 "fresh".to_owned(),
7713 "3".to_owned(),
7714 0,
7715 AttrMap::new(),
7716 )]);
7717 slab.add_keyed_edge("fresh", "3", 0, AttrMap::new());
7718 assert_slab_matches_mg(&mg, &slab);
7719 mg.extend_keyed_edges_with_attrs_unrecorded(vec![(
7722 "2".to_owned(),
7723 "fresh".to_owned(),
7724 7,
7725 AttrMap::new(),
7726 )]);
7727 slab.add_keyed_edge("2", "fresh", 7, AttrMap::new());
7728 assert_slab_matches_mg(&mg, &slab);
7729 assert_eq!(mg.remove_node("fresh"), slab.remove_node("fresh"));
7731 assert_slab_matches_mg(&mg, &slab);
7732 assert_eq!(mg.remove_node("1"), slab.remove_node("1"));
7734 assert_slab_matches_mg(&mg, &slab);
7735 assert_eq!(mg.remove_node("10"), slab.remove_node("10"));
7736 assert_slab_matches_mg(&mg, &slab);
7737 assert_eq!(mg.remove_node("gone"), slab.remove_node("gone"));
7739 assert_slab_matches_mg(&mg, &slab);
7740 }
7741
7742 fn assert_slab_edges_ordered_matches_mg(
7746 mg: &MultiGraph,
7747 slab: &super::multigraph_storage::MgSlabStorage,
7748 ) {
7749 let mg_ordered: Vec<(&str, &str, usize)> = mg
7750 .edges_ordered_borrowed()
7751 .into_iter()
7752 .map(|(l, r, k, _)| (l, r, k))
7753 .collect();
7754 assert_eq!(
7755 slab.edges_ordered_names(),
7756 mg_ordered,
7757 "edges_ordered walk emission must match"
7758 );
7759 assert_eq!(
7762 slab.edges_ordered_borrowed(),
7763 mg.edges_ordered_borrowed(),
7764 "attrs-carrying edges_ordered walk must match"
7765 );
7766 }
7767
7768 #[test]
7775 fn thp6w_s10_copy_walk_and_snapshot_orientation_gauntlet() {
7776 use super::multigraph_storage::MgSlabStorage;
7777
7778 let stream: Vec<(&str, &str, usize)> = vec![
7779 ("2", "10", 0),
7780 ("10", "2", 1),
7781 ("1", "1", 0),
7782 ("2", "3", 0),
7783 ("10", "1", 5),
7784 ("0", "2", 0),
7785 ("4", "5", 2),
7786 ("5", "4", 0),
7787 ("3", "0", 0),
7788 ("1", "1", 1),
7789 ("5", "0", 3),
7790 ];
7791 let mut mg = MultiGraph::strict();
7792 let _ = mg.extend_keyed_edges_with_attrs_unrecorded(
7793 stream
7794 .iter()
7795 .map(|(l, r, k)| ((*l).to_owned(), (*r).to_owned(), *k, AttrMap::new())),
7796 );
7797 let mut slab = MgSlabStorage::new();
7798 for (l, r, k) in &stream {
7799 slab.add_keyed_edge(l, r, *k, AttrMap::new());
7800 }
7801 assert_slab_matches_mg(&mg, &slab);
7802 assert_slab_edges_ordered_matches_mg(&mg, &slab);
7803
7804 mg.reorder_rows_for_nx_copy_walk();
7806 slab.reorder_rows_for_nx_copy_walk();
7807 assert_slab_matches_mg(&mg, &slab);
7808 assert_slab_edges_ordered_matches_mg(&mg, &slab);
7809
7810 mg.reorder_rows_for_nx_copy_walk();
7812 slab.reorder_rows_for_nx_copy_walk();
7813 assert_slab_matches_mg(&mg, &slab);
7814 assert_slab_edges_ordered_matches_mg(&mg, &slab);
7815
7816 mg.extend_keyed_edges_with_attrs_unrecorded(vec![
7818 ("0".to_owned(), "9".to_owned(), 0, AttrMap::new()),
7819 ("9".to_owned(), "2".to_owned(), 0, AttrMap::new()),
7820 ]);
7821 slab.add_keyed_edge("0", "9", 0, AttrMap::new());
7822 slab.add_keyed_edge("9", "2", 0, AttrMap::new());
7823 assert_slab_matches_mg(&mg, &slab);
7824 assert_slab_edges_ordered_matches_mg(&mg, &slab);
7825
7826 assert_eq!(mg.remove_node("2"), slab.remove_node("2"));
7828 mg.extend_keyed_edges_with_attrs_unrecorded(vec![(
7829 "fresh".to_owned(),
7830 "9".to_owned(),
7831 0,
7832 AttrMap::new(),
7833 )]);
7834 slab.add_keyed_edge("fresh", "9", 0, AttrMap::new());
7835 assert_slab_matches_mg(&mg, &slab);
7836 assert_slab_edges_ordered_matches_mg(&mg, &slab);
7837 mg.reorder_rows_for_nx_copy_walk();
7838 slab.reorder_rows_for_nx_copy_walk();
7839 assert_slab_matches_mg(&mg, &slab);
7840 assert_slab_edges_ordered_matches_mg(&mg, &slab);
7841 }
7842
7843 #[test]
7850 #[ignore = "paired A/B benchmark; run --release with --ignored --nocapture"]
7851 fn thp6w_s12_edges_ordered_read_ab() {
7852 use std::collections::HashMap;
7853 use std::hint::black_box;
7854 use std::time::Instant;
7855
7856 fn push_edge(
7857 stream: &mut Vec<(usize, usize, usize)>,
7858 key_counter: &mut HashMap<(usize, usize), usize>,
7859 u: usize,
7860 v: usize,
7861 ) {
7862 let entry = key_counter.entry((u.min(v), u.max(v))).or_insert(0);
7863 stream.push((u, v, *entry));
7864 *entry += 1;
7865 }
7866
7867 let n = 20000usize;
7868 let mut stream = Vec::new();
7869 let mut key_counter = HashMap::new();
7870 for i in 0..n {
7871 push_edge(&mut stream, &mut key_counter, i, (i + 1) % n);
7872 }
7873 let mut state = 0x2545_F491_4F6C_DD1D_u64;
7874 let draw = |state: &mut u64| -> usize {
7875 *state = state
7876 .wrapping_mul(6_364_136_223_846_793_005)
7877 .wrapping_add(1_442_695_040_888_963_407);
7878 usize::try_from(*state >> 33).unwrap() % n
7879 };
7880 for _ in 0..3 * n {
7881 let u = draw(&mut state);
7882 let v = draw(&mut state);
7883 push_edge(&mut stream, &mut key_counter, u, v);
7884 }
7885
7886 let mut g = MultiGraph::strict();
7887 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
7888 let slab = g.storage.clone();
7889 assert_eq!(
7890 slab.edges_ordered_borrowed(),
7891 g.edges_ordered_borrowed(),
7892 "arms must emit identical sequences"
7893 );
7894
7895 let time_string = || -> f64 {
7896 let t0 = Instant::now();
7897 let out = g.edges_ordered_borrowed();
7898 let dt = t0.elapsed().as_secs_f64();
7899 black_box(out.len());
7900 dt
7901 };
7902 let time_slab = || -> f64 {
7903 let t0 = Instant::now();
7904 let out = slab.edges_ordered_borrowed();
7905 let dt = t0.elapsed().as_secs_f64();
7906 black_box(out.len());
7907 dt
7908 };
7909 let median = |samples: &mut Vec<f64>| -> f64 {
7910 samples.sort_by(f64::total_cmp);
7911 samples[samples.len() / 2]
7912 };
7913 let (mut cur, mut sla, mut nul) = (Vec::new(), Vec::new(), Vec::new());
7914 for _ in 0..9 {
7915 cur.push(time_string());
7916 sla.push(time_slab());
7917 nul.push(time_string());
7918 }
7919 let (mc, ms, mn) = (median(&mut cur), median(&mut sla), median(&mut nul));
7920 println!(
7921 "THP6W_S12_EDGES_ORDERED_AB n={n} m={} string={mc:.6}s slab={ms:.6}s ratio_string_over_slab={:.3} null={:.3}",
7922 stream.len(),
7923 mc / ms,
7924 mc / mn,
7925 );
7926 }
7927
7928 #[test]
7935 #[ignore = "paired A/B benchmark; run --release with --ignored --nocapture"]
7936 fn thp6w_s13_edges_ordered_indices_ab() {
7937 use std::collections::HashMap;
7938 use std::hint::black_box;
7939 use std::time::Instant;
7940
7941 fn push_edge(
7942 stream: &mut Vec<(usize, usize, usize)>,
7943 key_counter: &mut HashMap<(usize, usize), usize>,
7944 u: usize,
7945 v: usize,
7946 ) {
7947 let entry = key_counter.entry((u.min(v), u.max(v))).or_insert(0);
7948 stream.push((u, v, *entry));
7949 *entry += 1;
7950 }
7951
7952 let n = 20000usize;
7953 let mut stream = Vec::new();
7954 let mut key_counter = HashMap::new();
7955 for i in 0..n {
7956 push_edge(&mut stream, &mut key_counter, i, (i + 1) % n);
7957 }
7958 let mut state = 0x2545_F491_4F6C_DD1D_u64;
7959 let draw = |state: &mut u64| -> usize {
7960 *state = state
7961 .wrapping_mul(6_364_136_223_846_793_005)
7962 .wrapping_add(1_442_695_040_888_963_407);
7963 usize::try_from(*state >> 33).unwrap() % n
7964 };
7965 for _ in 0..3 * n {
7966 let u = draw(&mut state);
7967 let v = draw(&mut state);
7968 push_edge(&mut stream, &mut key_counter, u, v);
7969 }
7970
7971 let mut g = MultiGraph::strict();
7972 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
7973 let slab = g.storage.clone();
7974 assert_eq!(
7975 slab.edges_ordered_indices_borrowed(),
7976 g.edges_ordered_indices_borrowed(),
7977 "arms must emit identical index sequences"
7978 );
7979
7980 let time_string = || -> f64 {
7981 let t0 = Instant::now();
7982 let out = g.edges_ordered_indices_borrowed();
7983 let dt = t0.elapsed().as_secs_f64();
7984 black_box(out.len());
7985 dt
7986 };
7987 let time_slab = || -> f64 {
7988 let t0 = Instant::now();
7989 let out = slab.edges_ordered_indices_borrowed();
7990 let dt = t0.elapsed().as_secs_f64();
7991 black_box(out.len());
7992 dt
7993 };
7994 let median = |samples: &mut Vec<f64>| -> f64 {
7995 samples.sort_by(f64::total_cmp);
7996 samples[samples.len() / 2]
7997 };
7998 let (mut cur, mut sla, mut nul) = (Vec::new(), Vec::new(), Vec::new());
7999 for _ in 0..9 {
8000 cur.push(time_string());
8001 sla.push(time_slab());
8002 nul.push(time_string());
8003 }
8004 let (mc, ms, mn) = (median(&mut cur), median(&mut sla), median(&mut nul));
8005 println!(
8006 "THP6W_S13_EDGES_ORDERED_INDICES_AB n={n} m={} string={mc:.6}s slab={ms:.6}s ratio_string_over_slab={:.3} null={:.3}",
8007 stream.len(),
8008 mc / ms,
8009 mc / mn,
8010 );
8011 }
8012
8013 fn assert_multigraph_storage_invariants(graph: &MultiGraph) {
8014 let storage = &graph.storage;
8015 assert_eq!(storage.node_order.slot_count(), storage.node_attrs.len());
8016 assert_eq!(storage.node_order.slot_count(), storage.rows.len());
8017 assert_eq!(graph.edge_count, storage.edge_count);
8018 assert_eq!(
8019 storage.edge_count,
8020 storage
8021 .edges
8022 .values()
8023 .map(super::multigraph_storage::CompactBucket::len)
8024 .sum::<usize>()
8025 );
8026
8027 let mut live_slots = vec![false; storage.node_order.slot_count()];
8028 for (position, (name, slot)) in storage.node_order.iter().enumerate() {
8029 assert!(!live_slots[slot], "slot {slot} is assigned twice");
8030 live_slots[slot] = true;
8031 assert_eq!(storage.node_order.name(slot), Some(name.as_str()));
8032 assert_eq!(graph.get_node_index(name), Some(position));
8034 assert_eq!(graph.get_node_name(position), Some(name.as_str()));
8035 assert_eq!(storage.slot_at_position(position), Some(slot));
8036 }
8037 assert_eq!(
8038 live_slots.iter().filter(|&&live| live).count(),
8039 graph.node_count()
8040 );
8041 assert_eq!(graph.get_node_name(graph.node_count()), None);
8042 assert_eq!(storage.slot_at_position(graph.node_count()), None);
8043
8044 let mut free_slots = BTreeSet::new();
8045 for &slot in storage.node_order.free_slots() {
8046 assert!(free_slots.insert(slot), "free slot {slot} listed twice");
8047 assert!(!live_slots[slot], "live slot {slot} is also free");
8048 assert!(storage.node_order.name(slot).is_none());
8049 assert!(storage.node_attrs[slot].is_empty());
8050 assert!(storage.rows[slot].is_empty());
8051 }
8052 assert_eq!(
8053 free_slots.len() + graph.node_count(),
8054 storage.node_order.slot_count(),
8055 "a slot is neither live nor free"
8056 );
8057
8058 for (left, row) in storage.rows.iter().enumerate() {
8059 if !live_slots[left] {
8060 assert!(row.is_empty(), "tombstoned slot {left} has neighbors");
8061 continue;
8062 }
8063 for (&right, keys) in row {
8064 assert!(live_slots[right], "row points to tombstoned slot {right}");
8065 let pair = (left.min(right), left.max(right));
8066 let bucket = storage
8067 .edges
8068 .get(&pair)
8069 .expect("every row cell must have an edge bucket");
8070 assert_eq!(keys.order(), bucket.key_order());
8071 if left != right {
8072 assert_eq!(
8073 storage.rows[right]
8074 .get(&left)
8075 .expect("undirected reverse row is missing")
8076 .order(),
8077 keys.order()
8078 );
8079 }
8080 }
8081 }
8082 for (&(left, right), bucket) in &storage.edges {
8083 assert!(live_slots[left] && live_slots[right]);
8084 assert_eq!(
8085 storage.rows[left]
8086 .get(&right)
8087 .expect("edge bucket missing left row")
8088 .order(),
8089 bucket.key_order()
8090 );
8091 if left != right {
8092 assert_eq!(
8093 storage.rows[right]
8094 .get(&left)
8095 .expect("edge bucket missing right row")
8096 .order(),
8097 bucket.key_order()
8098 );
8099 }
8100 }
8101 }
8102
8103 #[test]
8104 fn thp6w_s35_slab_is_sole_store_mutation_gauntlet() {
8105 let attr = |key: &str, value: i64| {
8106 let mut attrs = AttrMap::new();
8107 attrs.insert(key.to_owned(), fnx_runtime::CgseValue::Int(value));
8108 attrs
8109 };
8110
8111 let mut graph = MultiGraph::strict();
8112 assert!(graph.add_node_with_attrs("iso", attr("kind", 1)));
8113 assert_eq!(
8114 graph.add_edge_with_key_and_attrs("b", "a", 3, attr("weight", 2)),
8115 Ok(3)
8116 );
8117 assert_eq!(
8118 graph.add_edge_with_key_and_attrs("a", "b", 3, attr("color", 7)),
8119 Ok(3)
8120 );
8121 assert_eq!(
8122 graph.add_edge_with_key_and_attrs("a", "b", 0, AttrMap::new()),
8123 Ok(0)
8124 );
8125 assert_eq!(
8126 graph.add_edge_with_key_and_attrs("c", "c", 5, attr("weight", 11)),
8127 Ok(5)
8128 );
8129 assert_eq!(graph.nodes_ordered(), vec!["iso", "b", "a", "c"]);
8130 assert_eq!(graph.edge_keys_vec("a", "b"), vec![3, 0]);
8131 assert_eq!(
8132 graph.edge_attrs("a", "b", 3),
8133 Some(&{
8134 let mut attrs = attr("weight", 2);
8135 attrs.extend(attr("color", 7));
8136 attrs
8137 })
8138 );
8139 assert_eq!(graph.number_of_selfloops(), 1);
8140 assert_eq!(graph.degree("c"), 2);
8141 assert_eq!(graph.isolates(), vec!["iso"]);
8142 assert_multigraph_storage_invariants(&graph);
8143 assert_int_adjacency_matches(&graph);
8144
8145 let baseline = graph.snapshot();
8146 let cloned = graph.clone_with_fresh_policy();
8147 assert_eq!(cloned.snapshot(), baseline);
8148 assert_multigraph_storage_invariants(&cloned);
8149
8150 let revision = graph.revision();
8151 assert!(graph.replace_edge_attrs("a", "b", 3, attr("replacement", 9)));
8152 assert_eq!(graph.revision(), revision + 1);
8153 let revision = graph.revision();
8154 assert!(graph.replace_edge_attrs("a", "b", 3, attr("replacement", 9)));
8155 assert_eq!(
8156 graph.revision(),
8157 revision,
8158 "no-op edge replace bumped revision"
8159 );
8160 assert!(graph.replace_node_attrs("iso", attr("kind", 4)));
8161 assert!(graph.remove_edge("b", "a", Some(3)));
8162 assert_eq!(graph.edge_keys_vec("a", "b"), vec![0]);
8163 assert!(graph.remove_node("b"));
8164 assert!(!graph.has_edge("a", "b"));
8165 assert_eq!(graph.nodes_ordered(), vec!["iso", "a", "c"]);
8166 assert_eq!(graph.add_edge("d", "a"), Ok(0));
8167 assert_eq!(graph.nodes_ordered(), vec!["iso", "a", "c", "d"]);
8168 assert_multigraph_storage_invariants(&graph);
8169
8170 let (removed_nodes, removed_edges) = graph.remove_nodes_from(["iso", "c", "missing"]);
8171 assert_eq!((removed_nodes, removed_edges), (2, 1));
8172 assert_eq!(graph.nodes_ordered(), vec!["a", "d"]);
8173 assert_eq!(graph.edge_count(), 1);
8174 assert_multigraph_storage_invariants(&graph);
8175 graph.clear_edges();
8176 assert_eq!(graph.edge_count(), 0);
8177 assert_eq!(graph.isolates(), vec!["a", "d"]);
8178 assert_multigraph_storage_invariants(&graph);
8179 }
8180
8181 #[test]
8182 fn thp6w_s35_fresh_loaders_populate_sole_store() {
8183 let mut integer = MultiGraph::strict();
8184 assert_eq!(
8185 integer.extend_fresh_int_prefix_keyed_edges_unrecorded(
8186 4,
8187 [(0, 1, 0), (1, 0, 1), (2, 2, 0), (3, 0, 0)]
8188 ),
8189 4
8190 );
8191 assert_eq!(integer.nodes_ordered(), vec!["0", "1", "2", "3"]);
8192 assert_eq!(integer.edge_keys_vec("0", "1"), vec![0, 1]);
8193 assert_eq!(integer.number_of_selfloops(), 1);
8194 assert_multigraph_storage_invariants(&integer);
8195 assert_int_adjacency_matches(&integer);
8196
8197 let mut weight = AttrMap::new();
8198 weight.insert("weight".to_owned(), fnx_runtime::CgseValue::Int(2));
8199 let mut color = AttrMap::new();
8200 color.insert("color".to_owned(), fnx_runtime::CgseValue::Int(7));
8201 let mut attributed = MultiGraph::strict();
8202 assert_eq!(
8203 attributed.extend_fresh_index_keyed_edges_with_attrs_unrecorded(
8204 ["b", "a", "c"].into_iter().map(str::to_owned),
8205 [
8206 (0, 1, 4, weight),
8207 (1, 0, 4, color),
8208 (2, 2, 0, AttrMap::new()),
8209 (0, 9, 0, AttrMap::new()),
8210 ]
8211 ),
8212 2
8213 );
8214 assert_eq!(attributed.nodes_ordered(), vec!["b", "a", "c"]);
8215 assert_eq!(attributed.edge_count(), 2);
8216 assert_eq!(attributed.edge_keys_vec("a", "b"), vec![4]);
8217 let attrs = attributed
8218 .edge_attrs("a", "b", 4)
8219 .expect("merged attributed edge");
8220 assert_eq!(attrs.len(), 2);
8221 assert!(attrs.contains_key("weight"));
8222 assert!(attrs.contains_key("color"));
8223 assert_multigraph_storage_invariants(&attributed);
8224 }
8225
8226 #[test]
8230 fn multigraph_removal_runs_keep_positional_readers_in_step_with_names() {
8231 use std::collections::BTreeMap;
8232 let mut state = 0x9e37_79b9_7f4a_7c15_u64;
8233 let mut next = |bound: usize| {
8234 state ^= state << 13;
8235 state ^= state >> 7;
8236 state ^= state << 17;
8237 usize::try_from(state % bound as u64).unwrap()
8238 };
8239 let pair = |u: &str, v: &str| {
8240 if u <= v {
8241 (u.to_owned(), v.to_owned())
8242 } else {
8243 (v.to_owned(), u.to_owned())
8244 }
8245 };
8246 for round in 0..30 {
8247 let n = 10 + round % 17;
8248 let mut g = MultiGraph::strict();
8249 let mut order: Vec<String> = Vec::new();
8250 let mut edges: BTreeMap<(String, String), usize> = BTreeMap::new();
8251 let mut fresh = 0;
8252 let add_edge = |g: &mut MultiGraph,
8253 order: &mut Vec<String>,
8254 edges: &mut BTreeMap<(String, String), usize>,
8255 u: String,
8256 v: String| {
8257 for node in [&u, &v] {
8258 if !order.contains(node) {
8259 order.push(node.clone());
8260 }
8261 }
8262 *edges.entry(pair(&u, &v)).or_default() += 1;
8263 g.add_edge(u, v).expect("edge");
8264 };
8265 for _ in 0..3 * n {
8266 let (u, v) = (next(n), next(n));
8267 add_edge(
8268 &mut g,
8269 &mut order,
8270 &mut edges,
8271 format!("n{u}"),
8272 format!("n{v}"),
8273 );
8274 }
8275 for _ in 0..4 * n {
8276 match next(6) {
8277 0..=2 if !order.is_empty() => {
8278 let victim = order.remove(next(order.len()));
8279 edges.retain(|(u, v), _| *u != victim && *v != victim);
8280 assert!(g.remove_node(&victim));
8281 assert!(!g.remove_node(&victim));
8282 }
8283 3 | 4 => {
8284 let u = format!("f{fresh}");
8285 fresh += 1;
8286 let v = if order.is_empty() || next(4) == 0 {
8287 u.clone()
8288 } else {
8289 order[next(order.len())].clone()
8290 };
8291 add_edge(&mut g, &mut order, &mut edges, u, v);
8292 }
8293 5 if order.len() > 2 => {
8294 let doomed = [order[next(order.len())].clone(), "absent".to_owned()];
8295 let removed = g.remove_nodes_from(doomed.iter().map(String::as_str));
8296 assert_eq!(removed.0, 1);
8297 order.retain(|name| *name != doomed[0]);
8298 edges.retain(|(u, v), _| *u != doomed[0] && *v != doomed[0]);
8299 }
8300 _ => {}
8301 }
8302 assert_eq!(g.nodes_ordered(), order, "round {round}");
8303 assert_eq!(g.edge_count(), edges.values().sum::<usize>());
8304 for ((u, v), &count) in &edges {
8305 assert_eq!(g.edge_keys_vec(u, v).len(), count, "({u}, {v})");
8306 }
8307 assert_multigraph_storage_invariants(&g);
8308 assert_int_adjacency_matches(&g);
8309 }
8310 }
8311 }
8312
8313 #[cfg(any())]
8321 #[test]
8322 fn thp6w_s14_fresh_bulk_graph_is_warm_from_birth() {
8323 let mut g = MultiGraph::strict();
8324 let stream = vec![
8325 (0usize, 1usize, 0usize),
8326 (1, 2, 0),
8327 (0, 1, 1), (2, 2, 0), (3, 0, 0),
8330 ];
8331 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(5, stream.into_iter());
8332 assert!(
8333 g.slab_shadow_is_warm(),
8334 "fresh bulk graph must be born warm"
8335 );
8336 {
8337 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8338 assert_slab_matches_mg(&g, &shadow.1);
8339 }
8340 let _ = g.add_edge("2", "4");
8342 assert!(g.slab_shadow_is_warm());
8343 assert!(g.remove_edge("0", "1", None));
8344 assert!(g.slab_shadow_is_warm());
8345 assert!(g.remove_node("1"));
8346 assert!(g.slab_shadow_is_warm());
8347 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8348 assert_slab_matches_mg(&g, &shadow.1);
8349
8350 let mut nattrs = AttrMap::new();
8353 nattrs.insert("color".to_owned(), fnx_runtime::CgseValue::Int(7));
8354 let _ = g.add_node_with_attrs("2", nattrs);
8355 assert!(
8356 g.slab_shadow_is_warm(),
8357 "node-attr set must advance (not stale) the warm shadow"
8358 );
8359 {
8360 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8361 assert_slab_matches_mg(&g, &shadow.1);
8362 assert_eq!(
8363 shadow.1.node_attrs_for("2"),
8364 g.nodes.get("2"),
8365 "advanced shadow must carry the set node attr for node 2"
8366 );
8367 }
8368 let mut nattrs2 = AttrMap::new();
8370 nattrs2.insert("w".to_owned(), fnx_runtime::CgseValue::Int(9));
8371 let _ = g.add_node_with_attrs("attr_only", nattrs2);
8372 assert!(g.slab_shadow_is_warm());
8373 {
8374 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8375 assert_slab_matches_mg(&g, &shadow.1);
8376 assert_eq!(
8377 shadow.1.node_attrs_for("attr_only"),
8378 g.nodes.get("attr_only"),
8379 "advanced shadow must carry a new attr-only node"
8380 );
8381 }
8382 g.sync_slab_shadow();
8384 {
8385 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8386 assert_slab_matches_mg(&g, &shadow.1);
8387 }
8388
8389 let mut h = MultiGraph::strict();
8392 let mut w1 = AttrMap::new();
8393 w1.insert("w".to_owned(), fnx_runtime::CgseValue::Int(1));
8394 let mut w2 = AttrMap::new();
8395 w2.insert("x".to_owned(), fnx_runtime::CgseValue::Int(2));
8396 let _ = h.extend_fresh_index_keyed_edges_with_attrs_unrecorded(
8397 vec!["b".to_owned(), "a".to_owned(), "c".to_owned()],
8398 vec![
8399 (0, 1, 0, w1.clone()),
8400 (1, 0, 0, w2), (2, 2, 0, w1), (0, 9, 0, AttrMap::new()), (1, 2, 3, AttrMap::new()), ],
8405 );
8406 assert!(h.slab_shadow_is_warm(), "attributed fresh graph born warm");
8407 let shadow = h.slab_shadow.as_deref().expect("shadow present");
8408 assert_slab_matches_mg(&h, &shadow.1);
8409 assert_eq!(
8410 shadow.1.edges[&(0, 1)].attrs_for(0),
8411 h.edges
8412 .get(&super::EdgeKeyRef::new("b", "a"))
8413 .and_then(|kb| kb.get(&0)),
8414 "merged attrs on the dup cell must match"
8415 );
8416 }
8417
8418 #[cfg(any())]
8423 #[test]
8424 fn thp6w_s19_remove_nodes_from_advances_shadow() {
8425 let mut g = MultiGraph::strict();
8426 for i in 0..12u32 {
8427 let _ = g.add_edge(&i.to_string(), &((i + 1) % 12).to_string());
8428 }
8429 let _ = g.add_edge("0", "6"); let _ = g.add_edge("3", "3"); g.sync_slab_shadow();
8432 assert!(g.slab_shadow_is_warm());
8433
8434 let (rn, _) = g.remove_nodes_from(["3", "9"].into_iter());
8436 assert_eq!(rn, 2);
8437 assert!(
8438 g.slab_shadow_is_warm(),
8439 "remove_nodes_from fast path must advance the shadow"
8440 );
8441 {
8442 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8443 assert_slab_matches_mg(&g, &shadow.1);
8444 }
8445
8446 let (rn2, _) = g.remove_nodes_from(["0", "1", "2", "4", "5"].into_iter());
8448 assert_eq!(rn2, 5);
8449 assert!(
8450 g.slab_shadow_is_warm(),
8451 "remove_nodes_from fall-through must advance the shadow"
8452 );
8453 {
8454 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8455 assert_slab_matches_mg(&g, &shadow.1);
8456 }
8457
8458 g.add_node("island"); assert!(!g.slab_shadow_is_warm());
8461 let _ = g.remove_nodes_from(["island"].into_iter());
8462 assert!(
8463 !g.slab_shadow_is_warm(),
8464 "remove_nodes_from must not warm a stale shadow"
8465 );
8466 g.sync_slab_shadow();
8467 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8468 assert_slab_matches_mg(&g, &shadow.1);
8469 }
8470
8471 #[cfg(any())]
8475 #[test]
8476 fn thp6w_s20_extend_nodes_with_attrs_advances_shadow() {
8477 let mut g = MultiGraph::strict();
8478 for i in 0..6u32 {
8479 let _ = g.add_edge(&i.to_string(), &((i + 1) % 6).to_string());
8480 }
8481 g.sync_slab_shadow();
8482 assert!(g.slab_shadow_is_warm());
8483
8484 let attr = |k: &str, v: i64| {
8485 let mut a = AttrMap::new();
8486 a.insert(k.to_owned(), fnx_runtime::CgseValue::Int(v));
8487 a
8488 };
8489 let ins = g.extend_nodes_with_attrs_unrecorded(vec![
8491 ("new_a".to_owned(), attr("c", 1)),
8492 ("new_b".to_owned(), attr("c", 2)),
8493 ("0".to_owned(), attr("k", 3)),
8494 ]);
8495 assert_eq!(ins, 2, "two new nodes");
8496 assert!(
8497 g.slab_shadow_is_warm(),
8498 "node-attr batch must advance the shadow"
8499 );
8500 {
8501 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8502 assert_slab_matches_mg(&g, &shadow.1);
8503 assert_eq!(shadow.1.node_attrs_for("new_a"), g.nodes.get("new_a"));
8504 assert_eq!(shadow.1.node_attrs_for("0"), g.nodes.get("0"));
8505 }
8506
8507 let ins2 = g.extend_nodes_with_attrs_unrecorded(vec![("new_a".to_owned(), attr("c", 99))]);
8510 assert_eq!(ins2, 0, "no new nodes");
8511 assert!(
8512 g.slab_shadow_is_warm(),
8513 "attr-only batch keeps the shadow warm"
8514 );
8515 {
8516 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8517 assert_eq!(
8518 shadow.1.node_attrs_for("new_a"),
8519 g.nodes.get("new_a"),
8520 "attr-only update must be reflected in the (still-warm) shadow"
8521 );
8522 }
8523 }
8524
8525 #[cfg(any())]
8529 #[test]
8530 fn thp6w_s21_replace_node_attrs_advances_shadow() {
8531 let attr = |k: &str, v: i64| {
8532 let mut a = AttrMap::new();
8533 a.insert(k.to_owned(), fnx_runtime::CgseValue::Int(v));
8534 a
8535 };
8536 let mut g = MultiGraph::strict();
8537 let _ = g.add_node_with_attrs("x", attr("c", 1));
8538 let _ = g.add_edge("x", "y");
8539 g.sync_slab_shadow();
8540 assert!(g.slab_shadow_is_warm());
8541
8542 assert!(g.replace_node_attrs("x", attr("d", 2)));
8544 assert!(
8545 g.slab_shadow_is_warm(),
8546 "replace_node_attrs must advance the shadow"
8547 );
8548 {
8549 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8550 assert_slab_matches_mg(&g, &shadow.1);
8551 assert_eq!(
8552 shadow.1.node_attrs_for("x"),
8553 g.nodes.get("x"),
8554 "shadow must reflect the overwrite"
8555 );
8556 }
8557
8558 let same = g.nodes.get("x").cloned().unwrap_or_default();
8560 assert!(g.replace_node_attrs("x", same));
8561 assert!(g.slab_shadow_is_warm());
8562
8563 assert!(!g.replace_node_attrs("missing", attr("z", 9)));
8565 assert!(g.slab_shadow_is_warm());
8566 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8567 assert_slab_matches_mg(&g, &shadow.1);
8568 }
8569
8570 #[cfg(any())]
8574 #[test]
8575 fn thp6w_s22_replace_edge_attrs_advances_shadow() {
8576 let attr = |k: &str, v: i64| {
8577 let mut a = AttrMap::new();
8578 a.insert(k.to_owned(), fnx_runtime::CgseValue::Int(v));
8579 a
8580 };
8581 let mut g = MultiGraph::strict();
8582 let _ = g.add_edge_with_key_and_attrs("a", "b", 0, attr("w", 1));
8583 let _ = g.add_edge_with_key_and_attrs("a", "b", 1, attr("w", 2)); let _ = g.add_edge("b", "c");
8585 g.sync_slab_shadow();
8586 assert!(g.slab_shadow_is_warm());
8587
8588 assert!(g.replace_edge_attrs("a", "b", 0, attr("x", 5)));
8590 assert!(
8591 g.slab_shadow_is_warm(),
8592 "replace_edge_attrs must advance the shadow"
8593 );
8594 {
8595 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8596 assert_slab_matches_mg(&g, &shadow.1);
8597 assert_eq!(
8599 shadow.1.edges[&(0, 1)].attrs_for(0),
8600 g.edges
8601 .get(&super::EdgeKeyRef::new("a", "b"))
8602 .and_then(|kb| kb.get(&0)),
8603 "advanced shadow must carry the overwritten cell attrs"
8604 );
8605 assert_eq!(
8606 shadow.1.edges[&(0, 1)].attrs_for(1),
8607 g.edges
8608 .get(&super::EdgeKeyRef::new("a", "b"))
8609 .and_then(|kb| kb.get(&1)),
8610 "the parallel cell (key 1) must be untouched"
8611 );
8612 }
8613
8614 let same = g.edge_attrs("a", "b", 0).cloned().unwrap_or_default();
8616 assert!(g.replace_edge_attrs("a", "b", 0, same));
8617 assert!(g.slab_shadow_is_warm());
8618
8619 assert!(!g.replace_edge_attrs("a", "z", 0, attr("q", 1)));
8621 assert!(g.slab_shadow_is_warm());
8622 let shadow = g.slab_shadow.as_deref().expect("shadow present");
8623 assert_slab_matches_mg(&g, &shadow.1);
8624 }
8625
8626 #[cfg(any())]
8632 #[test]
8633 fn thp6w_s23_node_attrs_reads_route_to_slab() {
8634 let attr = |k: &str, v: i64| {
8635 let mut a = AttrMap::new();
8636 a.insert(k.to_owned(), fnx_runtime::CgseValue::Int(v));
8637 a
8638 };
8639 let mut g = MultiGraph::strict();
8640 let _ = g.add_node_with_attrs("x", attr("c", 1));
8641 let _ = g.add_edge("x", "y"); g.sync_slab_shadow();
8645 assert!(g.slab_shadow_is_warm());
8646 assert_eq!(
8648 g.node_attrs("x"),
8649 g.nodes.get("x"),
8650 "attr node routes to slab"
8651 );
8652 assert_eq!(g.node_attrs("y"), g.nodes.get("y"), "attr-less node routes");
8653 assert_eq!(g.node_attrs("missing"), g.nodes.get("missing"), "both None");
8654 assert!(g.node_attrs("x").is_some_and(|a| a.get("c").is_some()));
8655 assert!(g.replace_node_attrs("x", attr("d", 9)));
8657 assert!(g.slab_shadow_is_warm());
8658 assert_eq!(
8659 g.node_attrs("x"),
8660 g.nodes.get("x"),
8661 "routed read tracks overwrite"
8662 );
8663 let mut h = MultiGraph::strict();
8665 let _ = h.add_node_with_attrs("p", attr("c", 3));
8666 assert!(
8667 !h.slab_shadow_is_warm(),
8668 "fresh individual-add graph has no shadow"
8669 );
8670 assert_eq!(
8671 h.node_attrs("p"),
8672 h.nodes.get("p"),
8673 "unrouted fallback still correct"
8674 );
8675 }
8676
8677 #[cfg(any())]
8682 #[test]
8683 fn thp6w_s24_edge_attrs_reads_route_to_slab() {
8684 let attr = |k: &str, v: i64| {
8685 let mut a = AttrMap::new();
8686 a.insert(k.to_owned(), fnx_runtime::CgseValue::Int(v));
8687 a
8688 };
8689 let gt = |g: &MultiGraph, l: &str, r: &str, k: usize| {
8690 g.edges
8691 .get(&super::EdgeKeyRef::new(l, r))
8692 .and_then(|kb| kb.get(&k))
8693 .cloned()
8694 };
8695 let mut g = MultiGraph::strict();
8696 let _ = g.add_edge_with_key_and_attrs("a", "b", 0, attr("w", 1));
8697 let _ = g.add_edge_with_key_and_attrs("a", "b", 1, attr("w", 2)); let _ = g.add_edge("b", "c");
8699 g.sync_slab_shadow();
8700 assert!(g.slab_shadow_is_warm());
8701 assert_eq!(g.edge_attrs("a", "b", 0).cloned(), gt(&g, "a", "b", 0));
8703 assert_eq!(g.edge_attrs("a", "b", 1).cloned(), gt(&g, "a", "b", 1));
8704 assert_eq!(g.edge_attrs("a", "b", 9), None, "missing key");
8705 assert_eq!(g.edge_attrs("a", "z", 0), None, "missing edge");
8706 assert!(g.replace_edge_attrs("a", "b", 0, attr("x", 8)));
8708 assert!(g.slab_shadow_is_warm());
8709 assert_eq!(g.edge_attrs("a", "b", 0).cloned(), gt(&g, "a", "b", 0));
8710 let mut h = MultiGraph::strict();
8712 let _ = h.add_edge_with_key_and_attrs("p", "q", 0, attr("z", 5));
8713 assert!(!h.slab_shadow_is_warm());
8714 assert_eq!(h.edge_attrs("p", "q", 0).cloned(), gt(&h, "p", "q", 0));
8715 }
8716
8717 #[cfg(any())]
8722 #[test]
8723 fn thp6w_s25_edge_keys_vec_reads_route_to_slab() {
8724 let gt = |g: &MultiGraph, l: &str, r: &str| -> Vec<usize> {
8725 g.edges
8726 .get(&super::EdgeKeyRef::new(l, r))
8727 .map(|b| b.keys().copied().collect())
8728 .unwrap_or_default()
8729 };
8730 let mut g = MultiGraph::strict();
8731 let _ = g.add_edge("a", "b"); let _ = g.add_edge("a", "b"); let _ = g.add_edge("a", "b"); let _ = g.add_edge("b", "c");
8735 g.sync_slab_shadow();
8736 assert!(g.slab_shadow_is_warm());
8737 assert_eq!(
8738 g.edge_keys_vec("a", "b"),
8739 gt(&g, "a", "b"),
8740 "routed key order"
8741 );
8742 assert_eq!(g.edge_keys_vec("b", "c"), gt(&g, "b", "c"));
8743 assert_eq!(
8744 g.edge_keys_vec("a", "z"),
8745 Vec::<usize>::new(),
8746 "missing edge"
8747 );
8748 assert!(g.remove_edge("a", "b", Some(1)));
8750 assert!(g.slab_shadow_is_warm());
8751 assert_eq!(g.edge_keys_vec("a", "b"), gt(&g, "a", "b"), "after removal");
8752 let mut h = MultiGraph::strict();
8754 let _ = h.add_edge("p", "q");
8755 assert!(!h.slab_shadow_is_warm());
8756 assert_eq!(h.edge_keys_vec("p", "q"), gt(&h, "p", "q"));
8757 }
8758
8759 #[cfg(any())]
8764 #[test]
8765 fn thp6w_s26_neighbors_reads_route_to_slab() {
8766 fn gt<'a>(g: &'a MultiGraph, n: &str) -> Option<Vec<&'a str>> {
8767 g.adjacency
8768 .get(n)
8769 .map(|row| row.keys().map(String::as_str).collect())
8770 }
8771 let mut g = MultiGraph::strict();
8772 let _ = g.add_edge("a", "b");
8773 let _ = g.add_edge("a", "c");
8774 let _ = g.add_edge("a", "b"); let _ = g.add_edge("c", "d");
8776 g.sync_slab_shadow();
8777 assert!(g.slab_shadow_is_warm());
8778 assert_eq!(
8779 g.neighbors("a"),
8780 gt(&g, "a"),
8781 "routed distinct neighbors, row order"
8782 );
8783 assert_eq!(g.neighbors("c"), gt(&g, "c"));
8784 assert_eq!(g.neighbors("missing"), None, "absent node -> None");
8785 assert_eq!(g.neighbors("missing"), gt(&g, "missing"));
8786 assert!(g.remove_edge("a", "c", None));
8788 assert!(g.slab_shadow_is_warm());
8789 assert_eq!(g.neighbors("a"), gt(&g, "a"), "after removal");
8790 let mut h = MultiGraph::strict();
8792 let _ = h.add_edge("p", "q");
8793 assert!(!h.slab_shadow_is_warm());
8794 assert_eq!(h.neighbors("p"), gt(&h, "p"));
8795 }
8796
8797 #[cfg(any())]
8803 #[test]
8804 fn thp6w_s27_degree_reads_route_to_slab() {
8805 let gt = |g: &MultiGraph, n: &str| -> usize {
8806 g.adjacency.get(n).map_or(0, |row| {
8807 let mut d = 0;
8808 for (nbr, keys) in row {
8809 let c = keys.len();
8810 if nbr.as_str() == n {
8811 d += c * 2;
8812 } else {
8813 d += c;
8814 }
8815 }
8816 d
8817 })
8818 };
8819 let mut g = MultiGraph::strict();
8820 let _ = g.add_edge("a", "b");
8821 let _ = g.add_edge("a", "b"); let _ = g.add_edge("a", "c");
8823 let _ = g.add_edge("a", "a"); g.sync_slab_shadow();
8825 assert!(g.slab_shadow_is_warm());
8826 assert_eq!(g.degree("a"), gt(&g, "a"), "self-loop double + parallel");
8827 assert_eq!(g.degree("b"), gt(&g, "b"));
8828 assert_eq!(g.degree("missing"), 0, "absent node -> 0");
8829 assert_eq!(g.degree("missing"), gt(&g, "missing"));
8830 assert!(g.remove_edge("a", "b", None));
8832 assert!(g.slab_shadow_is_warm());
8833 assert_eq!(g.degree("a"), gt(&g, "a"), "after removal");
8834 let mut h = MultiGraph::strict();
8836 let _ = h.add_edge("p", "q");
8837 assert!(!h.slab_shadow_is_warm());
8838 assert_eq!(h.degree("p"), gt(&h, "p"));
8839 }
8840
8841 #[cfg(any())]
8846 #[test]
8847 fn thp6w_s28_membership_reads_route_to_slab() {
8848 let he = |g: &MultiGraph, l: &str, r: &str| {
8849 g.edges
8850 .get(&super::EdgeKeyRef::new(l, r))
8851 .is_some_and(|b| !b.is_empty())
8852 };
8853 let mut g = MultiGraph::strict();
8854 let _ = g.add_edge("a", "b");
8855 let _ = g.add_edge("b", "c");
8856 let _ = g.add_node("iso"); g.sync_slab_shadow();
8858 assert!(g.slab_shadow_is_warm());
8859 assert_eq!(g.has_node("a"), g.nodes.contains_key("a"));
8861 assert_eq!(g.has_node("iso"), g.nodes.contains_key("iso"));
8862 assert_eq!(g.has_node("missing"), g.nodes.contains_key("missing"));
8863 assert!(g.has_node("iso") && !g.has_node("missing"));
8864 assert_eq!(g.has_edge("a", "b"), he(&g, "a", "b"));
8866 assert_eq!(g.has_edge("b", "a"), he(&g, "b", "a"));
8867 assert_eq!(g.has_edge("a", "c"), he(&g, "a", "c"));
8868 assert_eq!(g.has_edge("x", "y"), he(&g, "x", "y"));
8869 assert!(g.has_edge("a", "b") && g.has_edge("b", "a") && !g.has_edge("a", "c"));
8870 assert!(g.remove_edge("a", "b", None));
8872 assert!(g.slab_shadow_is_warm());
8873 assert_eq!(g.has_edge("a", "b"), he(&g, "a", "b"));
8874 assert!(!g.has_edge("a", "b"));
8875 let mut h = MultiGraph::strict();
8877 let _ = h.add_edge("p", "q");
8878 assert!(!h.slab_shadow_is_warm());
8879 assert_eq!(h.has_edge("p", "q"), he(&h, "p", "q"));
8880 assert_eq!(h.has_node("p"), h.nodes.contains_key("p"));
8881 }
8882
8883 #[cfg(any())]
8888 #[test]
8889 fn thp6w_s29_nodes_ordered_reads_route_to_slab() {
8890 fn gt<'a>(g: &'a MultiGraph) -> Vec<&'a str> {
8891 g.nodes.keys().map(String::as_str).collect()
8892 }
8893 let mut g = MultiGraph::strict();
8894 let _ = g.add_edge("c", "a"); let _ = g.add_edge("a", "b"); let _ = g.add_node("z"); g.sync_slab_shadow();
8898 assert!(g.slab_shadow_is_warm());
8899 assert_eq!(g.nodes_ordered(), gt(&g), "routed node order == String");
8900 assert_eq!(
8901 g.nodes_ordered(),
8902 vec!["c", "a", "b", "z"],
8903 "insertion order"
8904 );
8905 assert!(g.remove_node("a"));
8907 assert!(g.slab_shadow_is_warm());
8908 assert_eq!(g.nodes_ordered(), gt(&g), "after removal");
8909 assert_eq!(g.nodes_ordered(), vec!["c", "b", "z"]);
8910 let mut h = MultiGraph::strict();
8912 let _ = h.add_edge("p", "q");
8913 assert!(!h.slab_shadow_is_warm());
8914 assert_eq!(h.nodes_ordered(), gt(&h));
8915 }
8916
8917 #[cfg(any())]
8923 #[test]
8924 fn thp6w_s30_is_isolate_reads_route_to_slab() {
8925 let gt = |g: &MultiGraph, n: &str| -> bool {
8926 g.nodes.contains_key(n) && g.adjacency.get(n).is_none_or(|row| row.is_empty())
8927 };
8928 let mut g = MultiGraph::strict();
8929 let _ = g.add_edge("a", "b");
8930 let _ = g.add_node("iso"); let _ = g.add_edge("s", "s"); g.sync_slab_shadow();
8933 assert!(g.slab_shadow_is_warm());
8934 assert_eq!(g.is_isolate("iso"), gt(&g, "iso"));
8935 assert!(g.is_isolate("iso"));
8936 assert_eq!(g.is_isolate("a"), gt(&g, "a"));
8937 assert!(!g.is_isolate("a"));
8938 assert_eq!(g.is_isolate("s"), gt(&g, "s"));
8939 assert!(!g.is_isolate("s"), "self-loop node is not isolated");
8940 assert_eq!(g.is_isolate("missing"), gt(&g, "missing"));
8941 assert!(!g.is_isolate("missing"));
8942 assert!(g.remove_edge("a", "b", None));
8944 assert!(g.slab_shadow_is_warm());
8945 assert_eq!(g.is_isolate("a"), gt(&g, "a"));
8946 assert!(g.is_isolate("a"));
8947 let mut h = MultiGraph::strict();
8949 let _ = h.add_node("lonely");
8950 assert!(!h.slab_shadow_is_warm());
8951 assert_eq!(h.is_isolate("lonely"), gt(&h, "lonely"));
8952 }
8953
8954 #[cfg(any())]
8960 #[test]
8961 fn thp6w_s31_weighted_size_int_routes_to_slab() {
8962 fn gt(g: &MultiGraph, w: &str) -> Option<i128> {
8963 let mut total = 0i128;
8964 for bucket in g.edges.values() {
8965 for attrs in bucket.values() {
8966 let v = match attrs.get(w) {
8967 Some(fnx_runtime::CgseValue::Int(x)) => i128::from(*x),
8968 Some(_) => return None,
8969 None => 1,
8970 };
8971 total = total.checked_add(v)?;
8972 }
8973 }
8974 Some(total)
8975 }
8976 let w = |v: i64| -> AttrMap {
8977 let mut a = AttrMap::new();
8978 a.insert("weight".to_owned(), fnx_runtime::CgseValue::Int(v));
8979 a
8980 };
8981 let mut g = MultiGraph::strict();
8982 let _ = g.add_edge_with_key_and_attrs("a", "b", 0, w(5));
8983 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();
8987 assert!(g.slab_shadow_is_warm());
8988 assert_eq!(
8989 g.weighted_size_int("weight"),
8990 gt(&g, "weight"),
8991 "routed == String"
8992 );
8993 assert_eq!(g.weighted_size_int("weight"), Some(11), "5+3+1+2");
8994 assert!(g.remove_edge("a", "b", Some(0)));
8996 assert!(g.slab_shadow_is_warm());
8997 assert_eq!(g.weighted_size_int("weight"), gt(&g, "weight"));
8998 assert_eq!(g.weighted_size_int("weight"), Some(6), "3+1+2");
8999 let mut fattr = AttrMap::new();
9001 fattr.insert("weight".to_owned(), fnx_runtime::CgseValue::Float(1.5));
9002 let _ = g.add_edge_with_key_and_attrs("d", "e", 0, fattr);
9003 assert!(g.slab_shadow_is_warm());
9004 assert_eq!(g.weighted_size_int("weight"), gt(&g, "weight"));
9005 assert!(
9006 g.weighted_size_int("weight").is_none(),
9007 "non-int weight -> None"
9008 );
9009 let mut h = MultiGraph::strict();
9011 let _ = h.add_edge_with_key_and_attrs("p", "q", 0, w(7));
9012 assert!(!h.slab_shadow_is_warm());
9013 assert_eq!(h.weighted_size_int("weight"), gt(&h, "weight"));
9014 assert_eq!(h.weighted_size_int("weight"), Some(7));
9015 }
9016
9017 #[cfg(any())]
9023 #[test]
9024 fn thp6w_s32_neighbors_iter_reads_route_to_slab() {
9025 fn gt<'a>(g: &'a MultiGraph, n: &str) -> Option<Vec<&'a str>> {
9026 g.adjacency
9027 .get(n)
9028 .map(|row| row.keys().map(String::as_str).collect())
9029 }
9030 let collect = |g: &MultiGraph, n: &str| -> Option<Vec<String>> {
9031 g.neighbors_iter(n)
9032 .map(|it| it.map(str::to_owned).collect())
9033 };
9034 let want = |g: &MultiGraph, n: &str| -> Option<Vec<String>> {
9035 gt(g, n).map(|v| v.into_iter().map(str::to_owned).collect())
9036 };
9037 let mut g = MultiGraph::strict();
9038 let _ = g.add_edge("a", "b");
9039 let _ = g.add_edge("a", "c");
9040 let _ = g.add_edge("a", "b"); let _ = g.add_edge("c", "d");
9042 g.sync_slab_shadow();
9043 assert!(g.slab_shadow_is_warm());
9044 assert_eq!(
9045 collect(&g, "a"),
9046 want(&g, "a"),
9047 "routed iter == String, row order"
9048 );
9049 assert_eq!(collect(&g, "c"), want(&g, "c"));
9050 assert!(g.neighbors_iter("missing").is_none(), "absent node -> None");
9051 assert!(g.remove_edge("a", "c", None));
9053 assert!(g.slab_shadow_is_warm());
9054 assert_eq!(collect(&g, "a"), want(&g, "a"), "after removal");
9055 let mut h = MultiGraph::strict();
9057 let _ = h.add_edge("p", "q");
9058 assert!(!h.slab_shadow_is_warm());
9059 assert_eq!(collect(&h, "p"), want(&h, "p"));
9060 }
9061
9062 #[cfg(any())]
9069 #[test]
9070 fn thp6w_s33_edge_keys_iter_reads_route_to_slab() {
9071 fn gt(g: &MultiGraph, l: &str, r: &str) -> Option<Vec<usize>> {
9072 g.adjacency
9073 .get(l)?
9074 .get(r)
9075 .map(|keys| keys.iter().copied().collect())
9076 }
9077 let collect = |g: &MultiGraph, l: &str, r: &str| -> Option<Vec<usize>> {
9078 g.edge_keys_iter(l, r).map(|it| it.copied().collect())
9079 };
9080 let mut g = MultiGraph::strict();
9081 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();
9086 assert!(g.slab_shadow_is_warm());
9087 assert_eq!(collect(&g, "a", "b"), gt(&g, "a", "b"), "Many == String");
9088 assert_eq!(collect(&g, "a", "b"), Some(vec![0, 1, 2]));
9089 assert_eq!(collect(&g, "b", "c"), gt(&g, "b", "c"), "One == String");
9090 assert_eq!(collect(&g, "b", "c"), Some(vec![0]));
9091 assert!(g.edge_keys_iter("a", "z").is_none(), "missing edge -> None");
9092 assert!(g.remove_edge("a", "b", Some(1)));
9094 assert!(g.slab_shadow_is_warm());
9095 assert_eq!(collect(&g, "a", "b"), gt(&g, "a", "b"), "after removal");
9096 assert_eq!(collect(&g, "a", "b"), Some(vec![0, 2]));
9097 let mut h = MultiGraph::strict();
9099 let _ = h.add_edge("p", "q");
9100 assert!(!h.slab_shadow_is_warm());
9101 assert_eq!(collect(&h, "p", "q"), gt(&h, "p", "q"));
9102 }
9103
9104 #[cfg(any())]
9112 #[test]
9113 fn thp6w_s11_slab_shadow_dual_write_gauntlet() {
9114 let shadow_parity = |g: &MultiGraph| {
9115 let shadow = g.slab_shadow.as_deref().expect("shadow must be present");
9116 assert_eq!(shadow.0, g.revision, "shadow must be warm (advanced)");
9117 assert_slab_matches_mg(g, &shadow.1);
9118 };
9119
9120 let mut g = MultiGraph::strict();
9121 let _ = g.add_edge("a", "b");
9122 g.sync_slab_shadow();
9123 assert!(g.slab_shadow_is_warm());
9124 shadow_parity(&g);
9125
9126 let _ = g.add_edge("b", "c"); shadow_parity(&g);
9129 let _ = g.add_edge("a", "b"); shadow_parity(&g);
9131 let _ = g.add_edge("d", "d"); shadow_parity(&g);
9133 let mut attrs = AttrMap::new();
9134 attrs.insert("w".to_owned(), fnx_runtime::CgseValue::Int(3));
9135 let _ = g.add_edge_with_key_and_attrs("a", "b", 0, attrs); shadow_parity(&g);
9137
9138 let _ = g.extend_keyed_edges_with_attrs_unrecorded(vec![
9140 ("x".to_owned(), "y".to_owned(), 0, AttrMap::new()),
9141 ("y".to_owned(), "x".to_owned(), 1, AttrMap::new()),
9142 ("a".to_owned(), "x".to_owned(), 0, AttrMap::new()),
9143 ]);
9144 shadow_parity(&g);
9145
9146 assert!(g.remove_edge("a", "b", None));
9149 shadow_parity(&g);
9150 assert!(g.remove_edge("a", "b", None));
9151 shadow_parity(&g);
9152 assert!(g.remove_node("b"));
9153 shadow_parity(&g);
9154 let _ = g.add_edge("recycled", "c");
9155 shadow_parity(&g);
9156
9157 g.reorder_rows_for_nx_copy_walk();
9161 assert!(
9162 g.slab_shadow_is_warm(),
9163 "apply_row_orders must advance (not drop) the warm shadow"
9164 );
9165 shadow_parity(&g);
9166 g.sync_slab_shadow();
9168 shadow_parity(&g);
9169
9170 g.add_node("island");
9172 assert!(
9173 !g.slab_shadow_is_warm(),
9174 "uninstrumented mutation must stale the shadow"
9175 );
9176 g.sync_slab_shadow();
9177 shadow_parity(&g);
9178 g.clear_edges();
9179 assert!(!g.slab_shadow_is_warm());
9180 g.sync_slab_shadow();
9181 shadow_parity(&g);
9182
9183 let _ = g.add_edge("r1", "r2"); g.add_node("stale_maker"); assert!(!g.slab_shadow_is_warm());
9190 let _ = g.add_edge("r2", "r1"); let _ = g.add_edge("r3", "r3");
9192 let expected: Vec<(String, String, usize, AttrMap)> = g
9193 .edges_ordered_borrowed() .into_iter()
9195 .map(|(l, r, k, a)| (l.to_owned(), r.to_owned(), k, a.clone()))
9196 .collect();
9197 let expected_owned: Vec<(String, String, usize, AttrMap)> = g
9199 .edges_ordered() .into_iter()
9201 .map(|e| (e.left, e.right, e.key, e.attrs))
9202 .collect();
9203 g.sync_slab_shadow();
9204 assert!(g.slab_shadow_is_warm());
9205 let served: Vec<(String, String, usize, AttrMap)> = g
9206 .edges_ordered_borrowed() .into_iter()
9208 .map(|(l, r, k, a)| (l.to_owned(), r.to_owned(), k, a.clone()))
9209 .collect();
9210 assert_eq!(
9211 served, expected,
9212 "shadow-served edges_ordered must equal the String walk"
9213 );
9214 let served_owned: Vec<(String, String, usize, AttrMap)> = g
9215 .edges_ordered() .into_iter()
9217 .map(|e| (e.left, e.right, e.key, e.attrs))
9218 .collect();
9219 assert_eq!(
9220 served_owned, expected_owned,
9221 "shadow-served owned edges_ordered must equal the String walk"
9222 );
9223 }
9224
9225 #[test]
9234 #[ignore = "paired A/B benchmark; run --release with --ignored --nocapture"]
9235 fn thp6w_s5_int_storage_construction_ab() {
9236 use super::multigraph_storage::MgIntStorageProto;
9237 use std::collections::HashMap;
9238 use std::hint::black_box;
9239 use std::time::Instant;
9240
9241 fn push_edge(
9242 stream: &mut Vec<(usize, usize, usize)>,
9243 key_counter: &mut HashMap<(usize, usize), usize>,
9244 u: usize,
9245 v: usize,
9246 ) {
9247 let entry = key_counter.entry((u.min(v), u.max(v))).or_insert(0);
9248 stream.push((u, v, *entry));
9249 *entry += 1;
9250 }
9251
9252 let n = 20000usize;
9253 let mut stream = Vec::new();
9254 let mut key_counter = HashMap::new();
9255 for i in 0..n {
9256 push_edge(&mut stream, &mut key_counter, i, (i + 1) % n);
9257 }
9258 let mut state = 0x2545_F491_4F6C_DD1D_u64;
9260 let draw = |state: &mut u64| -> usize {
9261 *state = state
9262 .wrapping_mul(6_364_136_223_846_793_005)
9263 .wrapping_add(1_442_695_040_888_963_407);
9264 usize::try_from(*state >> 33).unwrap() % n
9265 };
9266 for _ in 0..3 * n {
9267 let u = draw(&mut state);
9268 let v = draw(&mut state);
9269 push_edge(&mut stream, &mut key_counter, u, v);
9270 }
9271 for i in (0..2000).step_by(2) {
9272 push_edge(&mut stream, &mut key_counter, i, i + 1);
9273 }
9274
9275 let time_current = |stream: &[(usize, usize, usize)]| -> f64 {
9276 let t0 = Instant::now();
9277 let mut g = MultiGraph::strict();
9278 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
9279 let dt = t0.elapsed().as_secs_f64();
9280 black_box(&g);
9281 dt
9282 };
9283 let time_proto = |stream: &[(usize, usize, usize)]| -> f64 {
9284 let t0 = Instant::now();
9285 let proto = MgIntStorageProto::bulk_load_int_prefix(n, stream.iter().copied());
9286 let dt = t0.elapsed().as_secs_f64();
9287 black_box(&proto);
9288 dt
9289 };
9290 let time_compact = |stream: &[(usize, usize, usize)]| -> f64 {
9292 let t0 = Instant::now();
9293 let compact = super::multigraph_storage::MgIntStorageProtoCompact::bulk_load_int_prefix(
9294 n,
9295 stream.iter().copied(),
9296 );
9297 let dt = t0.elapsed().as_secs_f64();
9298 black_box(&compact);
9299 dt
9300 };
9301
9302 let mut g = MultiGraph::strict();
9305 let _ = g.extend_fresh_int_prefix_keyed_edges_unrecorded(n, stream.iter().copied());
9306 let proto = MgIntStorageProto::bulk_load_int_prefix(n, stream.iter().copied());
9307 let compact = super::multigraph_storage::MgIntStorageProtoCompact::bulk_load_int_prefix(
9308 n,
9309 stream.iter().copied(),
9310 );
9311 g.with_int_adjacency(|adj| {
9312 for (i, row) in adj.iter().enumerate() {
9313 let proto_row: Vec<usize> = proto.rows[i].keys().copied().collect();
9314 assert_eq!(row, &proto_row, "derived integer row {i} must agree");
9315 let compact_row: Vec<usize> = compact.rows[i].keys().copied().collect();
9316 assert_eq!(row, &compact_row, "compact integer row {i} must agree");
9317 }
9318 });
9319 assert_eq!(
9320 proto.edge_count,
9321 g.edge_count(),
9322 "A/B arms built different graphs"
9323 );
9324 assert_eq!(
9325 compact.edge_count,
9326 g.edge_count(),
9327 "compact arm built a different graph"
9328 );
9329 let time_slab = |stream: &[(usize, usize, usize)]| -> f64 {
9331 let t0 = Instant::now();
9332 let slab = super::multigraph_storage::MgSlabStorage::bulk_load_int_prefix(
9333 n,
9334 stream.iter().copied(),
9335 );
9336 let dt = t0.elapsed().as_secs_f64();
9337 black_box(&slab);
9338 dt
9339 };
9340 let slab = super::multigraph_storage::MgSlabStorage::bulk_load_int_prefix(
9341 n,
9342 stream.iter().copied(),
9343 );
9344 g.with_int_adjacency(|adj| {
9345 for (i, row) in adj.iter().enumerate() {
9346 let slab_row: Vec<usize> = slab.rows[i].keys().copied().collect();
9347 assert_eq!(row, &slab_row, "slab integer row {i} must agree");
9348 }
9349 });
9350 assert_eq!(
9351 slab.edge_count,
9352 g.edge_count(),
9353 "slab arm built a different graph"
9354 );
9355
9356 let median = |samples: &mut Vec<f64>| -> f64 {
9357 samples.sort_by(f64::total_cmp);
9358 samples[samples.len() / 2]
9359 };
9360 let (mut cur, mut pro, mut com, mut sla, mut nul) =
9361 (Vec::new(), Vec::new(), Vec::new(), Vec::new(), Vec::new());
9362 for _ in 0..9 {
9363 cur.push(time_current(&stream));
9364 pro.push(time_proto(&stream));
9365 com.push(time_compact(&stream));
9366 sla.push(time_slab(&stream));
9367 nul.push(time_current(&stream));
9368 }
9369 let (mc, mp, mk, ms, mn) = (
9370 median(&mut cur),
9371 median(&mut pro),
9372 median(&mut com),
9373 median(&mut sla),
9374 median(&mut nul),
9375 );
9376 println!(
9377 "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}",
9378 stream.len(),
9379 mc / mp,
9380 mc / mk,
9381 mc / ms,
9382 mk / ms,
9383 mc / mn,
9384 );
9385 }
9386
9387 #[test]
9388 fn thp6w_int_adjacency_cache_is_fresh_per_clone() {
9389 let mut g = MultiGraph::strict();
9390 for i in 0..4 {
9391 g.add_node(format!("n{i}"));
9392 }
9393 let _ = g.add_edge("n0", "n1");
9394 assert_int_adjacency_matches(&g);
9396 let mut clone = g.clone();
9399 let _ = clone.add_edge("n2", "n3");
9400 assert_int_adjacency_matches(&clone);
9401 assert_int_adjacency_matches(&g);
9402 let _ = g.add_edge("n0", "n2");
9404 assert_int_adjacency_matches(&g);
9405 assert_int_adjacency_matches(&clone);
9406 }
9407
9408 fn single_attr(key: &str, value: &str) -> AttrMap {
9409 let mut attrs = AttrMap::new();
9410 attrs.insert(key.to_owned(), CgseValue::from(value));
9411 attrs
9412 }
9413
9414 #[test]
9415 fn edge_slot_rows_follow_adj_order_and_refresh_after_mutation() {
9416 let mut graph = Graph::strict();
9417 let weighted = |value| {
9418 let mut attrs = AttrMap::new();
9419 attrs.insert("weight".to_owned(), CgseValue::Int(value));
9420 attrs
9421 };
9422 graph
9423 .add_edge_with_attrs("a", "b", weighted(1))
9424 .expect("edge add should succeed");
9425 graph
9426 .add_edge_with_attrs("a", "c", weighted(2))
9427 .expect("edge add should succeed");
9428 graph
9429 .add_edge_with_attrs("b", "c", weighted(3))
9430 .expect("edge add should succeed");
9431
9432 let a = graph.get_node_index("a").expect("a exists");
9433 graph.with_edge_slots_by_neighbor(|rows| {
9434 let weights = rows[a]
9435 .iter()
9436 .map(|&slot| {
9437 graph
9438 .edge_attrs_by_slot(slot)
9439 .and_then(|attrs| attrs.get("weight"))
9440 })
9441 .collect::<Vec<_>>();
9442 assert_eq!(
9443 weights,
9444 vec![Some(&CgseValue::Int(1)), Some(&CgseValue::Int(2))]
9445 );
9446 });
9447
9448 graph.apply_row_orders(&[("a".to_owned(), vec!["c".to_owned(), "b".to_owned()])]);
9449 graph.with_edge_slots_by_neighbor(|rows| {
9450 let weights = rows[a]
9451 .iter()
9452 .map(|&slot| {
9453 graph
9454 .edge_attrs_by_slot(slot)
9455 .and_then(|attrs| attrs.get("weight"))
9456 })
9457 .collect::<Vec<_>>();
9458 assert_eq!(
9459 weights,
9460 vec![Some(&CgseValue::Int(2)), Some(&CgseValue::Int(1))]
9461 );
9462 });
9463
9464 assert!(graph.remove_edge("a", "c"));
9465 graph.with_edge_slots_by_neighbor(|rows| {
9466 assert_eq!(rows[a].len(), 1);
9467 assert_eq!(
9468 graph
9469 .edge_attrs_by_slot(rows[a][0])
9470 .and_then(|attrs| attrs.get("weight")),
9471 Some(&CgseValue::Int(1))
9472 );
9473 });
9474 }
9475
9476 #[test]
9477 fn edges_ordered_tracks_node_and_neighbor_iteration_order() {
9478 let mut graph = Graph::strict();
9479 graph
9480 .add_edge_with_attrs("a", "b", AttrMap::new())
9481 .expect("edge add should succeed");
9482 graph
9483 .add_edge_with_attrs("b", "c", AttrMap::new())
9484 .expect("edge add should succeed");
9485 graph
9486 .add_edge_with_attrs("a", "c", AttrMap::new())
9487 .expect("edge add should succeed");
9488
9489 let pairs = graph
9490 .edges_ordered()
9491 .into_iter()
9492 .map(|edge| (edge.left, edge.right))
9493 .collect::<Vec<(String, String)>>();
9494 assert_eq!(
9495 pairs,
9496 vec![
9497 ("a".to_owned(), "b".to_owned()),
9498 ("a".to_owned(), "c".to_owned()),
9499 ("b".to_owned(), "c".to_owned()),
9500 ]
9501 );
9502 }
9503
9504 #[test]
9505 fn edges_ordered_preserves_traversed_endpoint_orientation() {
9506 let mut graph = Graph::strict();
9507 graph
9508 .add_edge_with_attrs("2", "1", AttrMap::new())
9509 .expect("edge add should succeed");
9510
9511 let pairs = graph
9512 .edges_ordered()
9513 .into_iter()
9514 .map(|edge| (edge.left, edge.right))
9515 .collect::<Vec<(String, String)>>();
9516 assert_eq!(pairs, vec![("2".to_owned(), "1".to_owned())]);
9517
9518 let borrowed_pairs = graph
9519 .edges_ordered_borrowed()
9520 .into_iter()
9521 .map(|(left, right, _)| (left.to_owned(), right.to_owned()))
9522 .collect::<Vec<(String, String)>>();
9523 assert_eq!(borrowed_pairs, vec![("2".to_owned(), "1".to_owned())]);
9524 }
9525
9526 #[test]
9527 fn edges_ordered_uses_node_order_for_preexisting_nodes() {
9528 let mut graph = Graph::strict();
9529 graph.add_node("1".to_owned());
9530 graph.add_node("2".to_owned());
9531 graph
9532 .add_edge_with_attrs("2", "1", AttrMap::new())
9533 .expect("edge add should succeed");
9534
9535 let pairs = graph
9536 .edges_ordered()
9537 .into_iter()
9538 .map(|edge| (edge.left, edge.right))
9539 .collect::<Vec<(String, String)>>();
9540 assert_eq!(pairs, vec![("1".to_owned(), "2".to_owned())]);
9541 }
9542
9543 #[test]
9544 fn edge_storage_order_index_iter_tracks_mutations() {
9545 let mut graph = Graph::strict();
9546 graph.add_node("c");
9547 graph.add_node("a");
9548 graph.add_node("b");
9549 graph
9550 .add_edge_with_attrs("c", "a", single_attr("weight", "1"))
9551 .expect("edge add should succeed");
9552 graph
9553 .add_edge_with_attrs("b", "c", single_attr("weight", "2"))
9554 .expect("edge add should succeed");
9555 graph
9556 .add_edge_with_attrs("a", "c", single_attr("color", "red"))
9557 .expect("duplicate edge update should succeed");
9558
9559 let indexed_edges = |graph: &Graph| {
9560 graph
9561 .edges_storage_order_index_iter()
9562 .map(|(left, right, attrs)| {
9563 (
9564 graph
9565 .get_node_name(left)
9566 .expect("left endpoint index should resolve")
9567 .to_owned(),
9568 graph
9569 .get_node_name(right)
9570 .expect("right endpoint index should resolve")
9571 .to_owned(),
9572 attrs.get("weight").cloned(),
9573 )
9574 })
9575 .collect::<Vec<_>>()
9576 };
9577
9578 assert_eq!(
9579 indexed_edges(&graph),
9580 vec![
9581 (
9582 "a".to_owned(),
9583 "c".to_owned(),
9584 Some(CgseValue::String("1".to_owned())),
9585 ),
9586 (
9587 "b".to_owned(),
9588 "c".to_owned(),
9589 Some(CgseValue::String("2".to_owned())),
9590 ),
9591 ]
9592 );
9593
9594 assert!(graph.remove_edge("a", "c"));
9595 assert_eq!(
9596 indexed_edges(&graph),
9597 vec![(
9598 "b".to_owned(),
9599 "c".to_owned(),
9600 Some(CgseValue::String("2".to_owned())),
9601 )]
9602 );
9603
9604 assert!(graph.remove_node("b"));
9605 assert!(indexed_edges(&graph).is_empty());
9606 }
9607
9608 fn assert_graph_core_invariants(graph: &Graph) {
9609 let mut unique_edges = BTreeSet::new();
9610 for node in graph.nodes_ordered() {
9611 let neighbors = graph
9612 .neighbors(node)
9613 .expect("graph nodes should always have adjacency buckets");
9614 for neighbor in neighbors {
9615 assert!(graph.has_node(neighbor));
9616 assert!(graph.has_edge(node, neighbor));
9617 let reverse_neighbors = graph
9618 .neighbors(neighbor)
9619 .expect("neighbor should always have adjacency bucket");
9620 assert!(reverse_neighbors.contains(&node));
9621 unique_edges.insert(canonical_edge(node, neighbor));
9622 }
9623 }
9624 assert_eq!(graph.edge_count(), unique_edges.len());
9625 }
9626
9627 fn assert_multigraph_core_invariants(graph: &MultiGraph) {
9628 let mut edge_instances = BTreeSet::new();
9629 for node in graph.nodes_ordered() {
9630 let neighbors = graph
9631 .neighbors(node)
9632 .expect("multigraph nodes should always have adjacency buckets");
9633 for neighbor in neighbors {
9634 assert!(graph.has_node(neighbor));
9635 assert!(graph.has_edge(node, neighbor));
9636 let reverse_neighbors = graph
9637 .neighbors(neighbor)
9638 .expect("neighbor should always have adjacency bucket");
9639 assert!(reverse_neighbors.contains(&node));
9640 let keys = graph
9641 .edge_keys(node, neighbor)
9642 .expect("parallel edge bucket should exist");
9643 for key in keys {
9644 let canonical = canonical_edge(node, neighbor);
9645 edge_instances.insert((canonical.0, canonical.1, key));
9646 }
9647 }
9648 }
9649 assert_eq!(graph.edge_count(), edge_instances.len());
9650 }
9651
9652 fn assert_decision_record_schema(record: &DecisionRecord, expected_mode: CompatibilityMode) {
9653 assert!(record.ts_unix_ms > 0);
9654 assert!(!record.operation.trim().is_empty());
9655 assert_eq!(record.mode, expected_mode);
9656 assert!((0.0..=1.0).contains(&record.incompatibility_probability));
9657 assert!(!record.rationale.trim().is_empty());
9658 assert!(!record.evidence.is_empty());
9659 for term in &record.evidence {
9660 assert!(!term.signal.trim().is_empty());
9661 assert!(!term.observed_value.trim().is_empty());
9662 }
9663 }
9664
9665 #[test]
9666 fn add_edge_autocreates_nodes_and_preserves_order() {
9667 let mut graph = Graph::strict();
9668 graph
9669 .add_edge_with_attrs("a", "b", AttrMap::new())
9670 .expect("edge insert should succeed");
9671 graph
9672 .add_edge_with_attrs("a", "c", AttrMap::new())
9673 .expect("edge insert should succeed");
9674
9675 assert_eq!(graph.node_count(), 3);
9676 assert_eq!(graph.edge_count(), 2);
9677 assert_eq!(graph.nodes_ordered(), vec!["a", "b", "c"]);
9678 assert_eq!(graph.neighbors("a"), Some(vec!["b", "c"]));
9679 }
9680
9681 #[test]
9682 fn add_edge_empty_endpoint_resolution_preserves_autocreation_effects() {
9683 let mut graph = Graph::strict();
9684 let before = graph.revision();
9685
9686 graph
9687 .add_edge("c", "a")
9688 .expect("fresh endpoints should be autocreated");
9689
9690 assert_eq!(graph.nodes_ordered(), vec!["c", "a"]);
9694 assert_eq!(graph.neighbors("c"), Some(vec!["a"]));
9695 assert_eq!(graph.neighbors("a"), Some(vec!["c"]));
9696 assert_eq!(graph.revision(), before + 3);
9697
9698 graph
9699 .add_edge("a", "c")
9700 .expect("an existing empty edge is a no-op");
9701 assert_eq!(graph.nodes_ordered(), vec!["c", "a"]);
9702 assert_eq!(graph.revision(), before + 3);
9703 }
9704
9705 #[test]
9706 fn extend_edges_unrecorded_preserves_adjacency_and_records_once() {
9707 let mut graph = Graph::strict();
9708 graph.add_node("a");
9709 graph.add_node("b");
9710 graph.add_node("c");
9711 let before = graph.evidence_ledger().records().len();
9712
9713 let inserted = graph.extend_edges_unrecorded([("a", "b"), ("a", "c"), ("b", "a")]);
9714
9715 assert_eq!(inserted, 2);
9716 assert_eq!(graph.edge_count(), 2);
9717 assert_eq!(graph.neighbors("a"), Some(vec!["b", "c"]));
9718 let records = graph.evidence_ledger().records();
9719 assert_eq!(records.len(), before + 1);
9720 assert_eq!(
9721 records.last().map(|record| record.operation.as_ref()),
9722 Some("extend_edges_unrecorded")
9723 );
9724 }
9725
9726 #[test]
9727 fn extend_existing_index_edges_unrecorded_matches_string_batch_on_int_prefix() {
9728 let nodes = (0..12).map(|node| node.to_string()).collect::<Vec<_>>();
9729 let edge_indices = vec![(10usize, 2usize), (2, 10), (3, 3), (1, 11), (11, 1)];
9730 let edge_strings = edge_indices
9731 .iter()
9732 .map(|(left, right)| (left.to_string(), right.to_string()))
9733 .collect::<Vec<_>>();
9734
9735 let mut by_string = Graph::strict();
9736 let mut by_index = Graph::strict();
9737 let _ = by_string.extend_nodes_unrecorded(nodes.clone());
9738 let _ = by_index.extend_nodes_unrecorded(nodes);
9739
9740 assert!(by_index.nodes_are_contiguous_int_prefix());
9741 assert_eq!(by_string.extend_edges_unrecorded(edge_strings), 3);
9742 assert_eq!(
9743 by_index.extend_existing_index_edges_unrecorded(edge_indices),
9744 3
9745 );
9746
9747 assert_eq!(by_index.snapshot(), by_string.snapshot());
9748 assert_graph_core_invariants(&by_index);
9749 }
9750
9751 #[test]
9752 fn extend_edges_with_attrs_unrecorded_matches_add_edge_with_attrs() {
9753 let mut attrs1 = AttrMap::new();
9758 attrs1.insert("w".to_owned(), CgseValue::Int(1));
9759 let mut attrs2 = AttrMap::new();
9760 attrs2.insert("w".to_owned(), CgseValue::Int(7));
9761 attrs2.insert("c".to_owned(), CgseValue::String("x".to_owned()));
9762
9763 let mut reference = Graph::strict();
9764 reference
9765 .add_edge_with_attrs("a", "b", attrs1.clone())
9766 .unwrap();
9767 reference
9768 .add_edge_with_attrs("b", "c", AttrMap::new())
9769 .unwrap();
9770 reference
9772 .add_edge_with_attrs("b", "a", attrs2.clone())
9773 .unwrap();
9774 reference
9776 .add_edge_with_attrs("d", "d", attrs1.clone())
9777 .unwrap();
9778
9779 let mut bulk = Graph::strict();
9780 let before = bulk.evidence_ledger().records().len();
9781 let inserted = bulk.extend_edges_with_attrs_unrecorded([
9782 ("a".to_owned(), "b".to_owned(), attrs1.clone()),
9783 ("b".to_owned(), "c".to_owned(), AttrMap::new()),
9784 ("b".to_owned(), "a".to_owned(), attrs2.clone()),
9785 ("d".to_owned(), "d".to_owned(), attrs1.clone()),
9786 ]);
9787
9788 assert_eq!(inserted, 3);
9789 assert_eq!(bulk.edge_count(), reference.edge_count());
9790 assert_eq!(bulk.nodes_ordered(), reference.nodes_ordered());
9791 for node in bulk.nodes_ordered() {
9792 assert_eq!(bulk.neighbors(node), reference.neighbors(node));
9793 }
9794 assert_eq!(bulk.edge_attrs("a", "b"), reference.edge_attrs("a", "b"));
9795 assert_eq!(bulk.edge_attrs("d", "d"), reference.edge_attrs("d", "d"));
9796 assert_eq!(bulk.evidence_ledger().records().len(), before + 1);
9798 }
9799
9800 #[test]
9801 fn multigraph_int_prefix_keyed_edges_match_string_batch() {
9802 let indexed_edges = vec![
9803 (0usize, 1usize, 0usize),
9804 (1, 2, 0),
9805 (2, 0, 0),
9806 (0, 1, 1),
9807 (2, 2, 0),
9808 ];
9809 let string_edges = indexed_edges
9810 .iter()
9811 .map(|(left, right, key)| (left.to_string(), right.to_string(), *key, AttrMap::new()))
9812 .collect::<Vec<_>>();
9813
9814 let mut by_string = MultiGraph::strict();
9815 let mut by_index = MultiGraph::strict();
9816
9817 assert_eq!(
9818 by_string.extend_keyed_edges_with_attrs_unrecorded(string_edges),
9819 indexed_edges.len()
9820 );
9821 assert_eq!(
9822 by_index.extend_fresh_int_prefix_keyed_edges_unrecorded(3, indexed_edges),
9823 5
9824 );
9825
9826 assert_eq!(by_index.snapshot(), by_string.snapshot());
9827 assert_multigraph_core_invariants(&by_index);
9828 }
9829
9830 #[test]
9831 fn extend_nodes_unrecorded_preserves_order_and_records_once() {
9832 let mut graph = Graph::strict();
9833 graph.add_node("1");
9834 let before = graph.evidence_ledger().records().len();
9835
9836 let inserted =
9837 graph.extend_nodes_unrecorded(["0".to_owned(), "1".to_owned(), "2".to_owned()]);
9838
9839 assert_eq!(inserted, 2);
9840 assert_eq!(graph.node_count(), 3);
9841 assert_eq!(graph.nodes_ordered(), vec!["1", "0", "2"]);
9842 assert_eq!(graph.neighbor_count("0"), 0);
9843 assert_eq!(graph.neighbor_count("2"), 0);
9844 let records = graph.evidence_ledger().records();
9845 assert_eq!(records.len(), before + 1);
9846 assert_eq!(
9847 records.last().map(|record| record.operation.as_ref()),
9848 Some("extend_nodes_unrecorded")
9849 );
9850 }
9851
9852 #[test]
9853 fn neighbors_iter_preserves_deterministic_order() {
9854 let mut graph = Graph::strict();
9855 graph.add_edge("a", "b").expect("edge add should succeed");
9856 graph.add_edge("a", "c").expect("edge add should succeed");
9857 graph.add_edge("a", "d").expect("edge add should succeed");
9858
9859 let neighbors = graph
9860 .neighbors_iter("a")
9861 .expect("neighbors should exist")
9862 .collect::<Vec<&str>>();
9863 assert_eq!(neighbors, vec!["b", "c", "d"]);
9864 }
9865
9866 #[test]
9867 fn neighbor_count_matches_neighbors_len() {
9868 let mut graph = Graph::strict();
9869 graph.add_edge("a", "b").expect("edge add should succeed");
9870 graph.add_edge("a", "c").expect("edge add should succeed");
9871 assert_eq!(graph.neighbor_count("a"), 2);
9872 assert_eq!(graph.neighbor_count("missing"), 0);
9873 }
9874
9875 #[test]
9876 fn repeated_edge_updates_attrs_in_place() {
9877 let mut graph = Graph::strict();
9878 let mut first = AttrMap::new();
9879 first.insert("weight".to_owned(), "1".into());
9880 graph
9881 .add_edge_with_attrs("a", "b", first)
9882 .expect("edge insert should succeed");
9883
9884 let mut second = AttrMap::new();
9885 second.insert("color".to_owned(), "blue".into());
9886 graph
9887 .add_edge_with_attrs("b", "a", second)
9888 .expect("edge update should succeed");
9889
9890 let attrs = graph
9891 .edge_attrs("a", "b")
9892 .expect("edge attrs should be present");
9893 assert_eq!(
9894 attrs.get("weight"),
9895 Some(&CgseValue::String("1".to_owned()))
9896 );
9897 assert_eq!(
9898 attrs.get("color"),
9899 Some(&CgseValue::String("blue".to_owned()))
9900 );
9901 assert_eq!(graph.edge_count(), 1);
9902 }
9903
9904 #[test]
9905 fn complete_graph_bulk_constructor_preserves_ordered_contract() {
9906 let graph = Graph::complete_graph(CompatibilityMode::Strict, 5);
9907
9908 assert_eq!(graph.node_count(), 5);
9909 assert_eq!(graph.edge_count(), 10);
9910 assert_eq!(graph.nodes_ordered(), vec!["0", "1", "2", "3", "4"]);
9911 assert_eq!(
9912 graph.neighbors("0").expect("node 0 should exist"),
9913 vec!["1", "2", "3", "4"]
9914 );
9915 assert_eq!(
9916 graph.neighbors("4").expect("node 4 should exist"),
9917 vec!["0", "1", "2", "3"]
9918 );
9919 assert_eq!(
9920 graph
9921 .edges_ordered()
9922 .into_iter()
9923 .map(|edge| (edge.left, edge.right))
9924 .collect::<Vec<_>>(),
9925 vec![
9926 ("0".to_owned(), "1".to_owned()),
9927 ("0".to_owned(), "2".to_owned()),
9928 ("0".to_owned(), "3".to_owned()),
9929 ("0".to_owned(), "4".to_owned()),
9930 ("1".to_owned(), "2".to_owned()),
9931 ("1".to_owned(), "3".to_owned()),
9932 ("1".to_owned(), "4".to_owned()),
9933 ("2".to_owned(), "3".to_owned()),
9934 ("2".to_owned(), "4".to_owned()),
9935 ("3".to_owned(), "4".to_owned()),
9936 ]
9937 );
9938
9939 let operations = graph
9940 .evidence_ledger()
9941 .records()
9942 .iter()
9943 .map(|record| record.operation.as_ref())
9944 .collect::<Vec<_>>();
9945 assert_eq!(operations, vec!["complete_graph_bulk"]);
9946 }
9947
9948 #[test]
9949 fn remove_node_removes_incident_edges() {
9950 let mut graph = Graph::strict();
9951 graph.add_edge("a", "b").expect("edge add should succeed");
9952 graph.add_edge("b", "c").expect("edge add should succeed");
9953 assert!(graph.remove_node("b"));
9954 assert_eq!(graph.node_count(), 2);
9955 assert_eq!(graph.edge_count(), 0);
9956 }
9957
9958 #[test]
9969 #[ignore = "measurement; run with --release --ignored --nocapture"]
9970 fn ledger_record_cost_ab() {
9971 use fnx_runtime::{EvidenceTerm, RuntimePolicy, bool_evidence, count_evidence};
9972 use std::hint::black_box;
9973 use std::time::Instant;
9974
9975 const EDGES: usize = 20_000;
9976 const ROUNDS: usize = 21;
9977 const RATIONALE: &str = "argmin expected loss over {allow,full_validate,fail_closed}";
9978
9979 fn gate_terms() -> Vec<EvidenceTerm> {
9982 vec![EvidenceTerm {
9983 signal: "unknown_incompatible_feature".into(),
9984 observed_value: bool_evidence(false),
9985 log_likelihood_ratio: 12.0,
9986 }]
9987 }
9988 fn outcome_terms(edge_attr_count: usize) -> Vec<EvidenceTerm> {
9989 vec![
9990 EvidenceTerm {
9991 signal: "self_loop".into(),
9992 observed_value: bool_evidence(false),
9993 log_likelihood_ratio: -0.5,
9994 },
9995 EvidenceTerm {
9996 signal: "edge_attr_count".into(),
9997 observed_value: count_evidence(edge_attr_count),
9998 log_likelihood_ratio: -2.0,
9999 },
10000 EvidenceTerm {
10001 signal: "left_autocreated".into(),
10002 observed_value: bool_evidence(true),
10003 log_likelihood_ratio: -1.25,
10004 },
10005 EvidenceTerm {
10006 signal: "right_autocreated".into(),
10007 observed_value: bool_evidence(true),
10008 log_likelihood_ratio: -1.25,
10009 },
10010 ]
10011 }
10012
10013 fn node_terms() -> Vec<EvidenceTerm> {
10019 vec![
10020 EvidenceTerm {
10021 signal: "node_preexisting".into(),
10022 observed_value: bool_evidence(false),
10023 log_likelihood_ratio: -3.0,
10024 },
10025 EvidenceTerm {
10026 signal: "attrs_count".into(),
10027 observed_value: count_evidence(0),
10028 log_likelihood_ratio: -1.0,
10029 },
10030 ]
10031 }
10032
10033 let recorded = || {
10035 let mut policy = RuntimePolicy::strict();
10036 let start = Instant::now();
10037 for index in 0..EDGES {
10038 let action = policy.action_for(0.08, false);
10039 policy.record("add_edge", action, 0.08, RATIONALE, gate_terms());
10040 policy.record(
10041 "add_edge",
10042 action,
10043 0.08,
10044 RATIONALE,
10045 outcome_terms(index & 3),
10046 );
10047 }
10048 let elapsed = start.elapsed();
10049 assert_eq!(policy.decision_log().total_recorded(), (EDGES * 2) as u64);
10050 elapsed
10051 };
10052
10053 let recorded_single = || {
10057 let mut policy = RuntimePolicy::strict();
10058 let start = Instant::now();
10059 for index in 0..EDGES {
10060 let action = policy.action_for(0.08, false);
10061 policy.record(
10062 "add_edge",
10063 action,
10064 0.08,
10065 RATIONALE,
10066 outcome_terms(index & 3),
10067 );
10068 }
10069 let elapsed = start.elapsed();
10070 assert_eq!(policy.decision_log().total_recorded(), EDGES as u64);
10071 elapsed
10072 };
10073
10074 let recorded_edge_build = || {
10082 let mut policy = RuntimePolicy::strict();
10083 let start = Instant::now();
10084 for index in 0..EDGES {
10085 let node_action = policy.action_for(0.0, false);
10086 policy.record("add_node", node_action, 0.0, RATIONALE, node_terms());
10087 let action = policy.action_for(0.08, false);
10088 policy.record(
10089 "add_edge",
10090 action,
10091 0.08,
10092 RATIONALE,
10093 outcome_terms(index & 3),
10094 );
10095 }
10096 let elapsed = start.elapsed();
10097 assert_eq!(policy.decision_log().total_recorded(), (EDGES * 2) as u64);
10098 elapsed
10099 };
10100
10101 let unbounded = || {
10105 let policy = RuntimePolicy::strict();
10106 let mode = policy.mode();
10107 let mut records: Vec<DecisionRecord> = Vec::new();
10108 let start = Instant::now();
10109 for index in 0..EDGES {
10110 let action = policy.action_for(0.08, false);
10111 records.push(DecisionRecord {
10112 ts_unix_ms: fnx_runtime::unix_time_ms(),
10113 operation: "add_edge".into(),
10114 mode,
10115 action,
10116 incompatibility_probability: 0.08,
10117 rationale: RATIONALE.into(),
10118 evidence: gate_terms(),
10119 });
10120 records.push(DecisionRecord {
10121 ts_unix_ms: fnx_runtime::unix_time_ms(),
10122 operation: "add_edge".into(),
10123 mode,
10124 action,
10125 incompatibility_probability: 0.08,
10126 rationale: RATIONALE.into(),
10127 evidence: outcome_terms(index & 3),
10128 });
10129 }
10130 let elapsed = start.elapsed();
10131 assert_eq!(records.len(), EDGES * 2);
10132 elapsed
10133 };
10134
10135 let discarded = || {
10137 let policy = RuntimePolicy::strict();
10138 let start = Instant::now();
10139 for index in 0..EDGES {
10140 let action = policy.action_for(0.08, false);
10141 black_box((action, "add_edge", 0.08, RATIONALE, gate_terms()));
10142 black_box((
10143 action,
10144 "add_edge",
10145 0.08,
10146 RATIONALE,
10147 outcome_terms(index & 3),
10148 ));
10149 }
10150 let elapsed = start.elapsed();
10151 assert!(policy.decision_log().records().is_empty());
10152 elapsed
10153 };
10154
10155 let clocked = || {
10157 let policy = RuntimePolicy::strict();
10158 let start = Instant::now();
10159 for index in 0..EDGES {
10160 let action = policy.action_for(0.08, false);
10161 black_box((action, fnx_runtime::unix_time_ms(), gate_terms()));
10162 black_box((
10163 action,
10164 fnx_runtime::unix_time_ms(),
10165 outcome_terms(index & 3),
10166 ));
10167 }
10168 start.elapsed()
10169 };
10170
10171 let materialized = || {
10175 let policy = RuntimePolicy::strict();
10176 let mode = policy.mode();
10177 let start = Instant::now();
10178 for index in 0..EDGES {
10179 let action = policy.action_for(0.08, false);
10180 black_box(DecisionRecord {
10181 ts_unix_ms: fnx_runtime::unix_time_ms(),
10182 operation: "add_edge".into(),
10183 mode,
10184 action,
10185 incompatibility_probability: 0.08,
10186 rationale: RATIONALE.into(),
10187 evidence: gate_terms(),
10188 });
10189 black_box(DecisionRecord {
10190 ts_unix_ms: fnx_runtime::unix_time_ms(),
10191 operation: "add_edge".into(),
10192 mode,
10193 action,
10194 incompatibility_probability: 0.08,
10195 rationale: RATIONALE.into(),
10196 evidence: outcome_terms(index & 3),
10197 });
10198 }
10199 start.elapsed()
10200 };
10201
10202 let add_edge_full = || {
10204 let mut graph = Graph::strict();
10205 let keys = (0..=EDGES).map(|n| n.to_string()).collect::<Vec<_>>();
10206 let start = Instant::now();
10207 for index in 0..EDGES {
10208 graph
10209 .add_edge_with_attrs(
10210 keys[index].clone(),
10211 keys[index + 1].clone(),
10212 AttrMap::new(),
10213 )
10214 .expect("edge add should succeed");
10215 }
10216 let elapsed = start.elapsed();
10217 assert_eq!(graph.edge_count(), EDGES);
10218 elapsed
10219 };
10220
10221 let mut record_ns = Vec::with_capacity(ROUNDS);
10222 let mut single_ns = Vec::with_capacity(ROUNDS);
10223 let mut edge_build_ns = Vec::with_capacity(ROUNDS);
10224 let mut unbounded_ns = Vec::with_capacity(ROUNDS);
10225 let mut discard_ns = Vec::with_capacity(ROUNDS);
10226 let mut clock_ns = Vec::with_capacity(ROUNDS);
10227 let mut material_ns = Vec::with_capacity(ROUNDS);
10228 let mut full_ns = Vec::with_capacity(ROUNDS);
10229 for _ in 0..ROUNDS {
10230 record_ns.push(recorded().as_nanos() as f64 / EDGES as f64);
10231 single_ns.push(recorded_single().as_nanos() as f64 / EDGES as f64);
10232 edge_build_ns.push(recorded_edge_build().as_nanos() as f64 / EDGES as f64);
10233 unbounded_ns.push(unbounded().as_nanos() as f64 / EDGES as f64);
10234 discard_ns.push(discarded().as_nanos() as f64 / EDGES as f64);
10235 clock_ns.push(clocked().as_nanos() as f64 / EDGES as f64);
10236 material_ns.push(materialized().as_nanos() as f64 / EDGES as f64);
10237 full_ns.push(add_edge_full().as_nanos() as f64 / EDGES as f64);
10238 }
10239 let median = |samples: &mut Vec<f64>| {
10240 samples.sort_by(f64::total_cmp);
10241 samples[samples.len() / 2]
10242 };
10243 let recorded_ns = median(&mut record_ns);
10244 let single_median_ns = median(&mut single_ns);
10245 let edge_build_median_ns = median(&mut edge_build_ns);
10246 let unbounded_median_ns = median(&mut unbounded_ns);
10247 let discarded_ns = median(&mut discard_ns);
10248 let clocked_ns = median(&mut clock_ns);
10249 let materialized_ns = median(&mut material_ns);
10250 let full = median(&mut full_ns);
10251 println!("ledger_record_cost_ab n={EDGES} rounds={ROUNDS} (2 records/edge)");
10252 println!(" A bounded ledger, 2 records: {recorded_ns:8.1} ns/edge");
10253 println!(" A1 bounded ledger, 1 record : {single_median_ns:8.1} ns/edge (SHIPPED)");
10254 println!(
10255 " A2 two records per edge : {edge_build_median_ns:8.1} ns/edge (pre-wa1b9 counterfactual)"
10256 );
10257 println!(" A0 unbounded Vec (incumbent): {unbounded_median_ns:8.1} ns/edge");
10258 println!(" B build terms only : {discarded_ns:8.1} ns/edge");
10259 println!(" C terms + clock : {clocked_ns:8.1} ns/edge");
10260 println!(" D terms + clock + record : {materialized_ns:8.1} ns/edge");
10261 println!(" add_edge_with_attrs (total) : {full:8.1} ns/edge");
10262 println!(
10263 " -> clock (C-B) : {:8.1} ns/edge",
10264 clocked_ns - discarded_ns
10265 );
10266 println!(
10267 " -> static str clones (D-C) : {:8.1} ns/edge",
10268 materialized_ns - clocked_ns
10269 );
10270 println!(
10271 " -> unbounded retention (A0-D): {:8.1} ns/edge",
10272 unbounded_median_ns - materialized_ns
10273 );
10274 println!(
10275 " -> bounded retention (A-D) : {:8.1} ns/edge",
10276 recorded_ns - materialized_ns
10277 );
10278 println!(
10279 " -> bound speedup (A0/A) : {:8.4}x",
10280 unbounded_median_ns / recorded_ns
10281 );
10282 println!(
10283 " -> single-record (A/A1) : {:8.4}x",
10284 recorded_ns / single_median_ns
10285 );
10286 println!(
10287 " -> both levers (A0/A1) : {:8.4}x",
10288 unbounded_median_ns / single_median_ns
10289 );
10290 println!(
10291 " -> ledger share, SHIPPED : {:6.1}% (A1/full — one record per add_edge)",
10292 single_median_ns / full * 100.0
10293 );
10294 println!(
10295 " -> ledger share, pre-wa1b9 : {:6.1}% (A2/full — when autocreation also recorded)",
10296 edge_build_median_ns / full * 100.0
10297 );
10298 }
10299
10300 #[test]
10331 #[ignore = "measurement; run with --release --ignored --nocapture"]
10332 fn ledger_cow_field_cost_ab() {
10333 use fnx_runtime::{
10334 DecisionAction, EvidenceTerm, RuntimePolicy, bool_evidence, count_evidence,
10335 };
10336 use std::borrow::Cow;
10337 use std::hint::black_box;
10338 use std::time::{Duration, Instant};
10339
10340 const EDGES: usize = 20_000;
10341 const ROUNDS: usize = 21;
10342 const CAP: usize = fnx_runtime::DECISION_LEDGER_RETAINED_RECORDS;
10343 const RATIONALE: &str = "argmin expected loss over {allow,full_validate,fail_closed}";
10344
10345 struct CowRecord {
10347 ts_unix_ms: u128,
10348 operation: Cow<'static, str>,
10349 action: DecisionAction,
10350 incompatibility_probability: f64,
10351 rationale: Cow<'static, str>,
10352 evidence: Vec<CowTerm>,
10353 }
10354 struct CowTerm {
10355 signal: Cow<'static, str>,
10356 observed_value: Cow<'static, str>,
10357 log_likelihood_ratio: f64,
10358 }
10359
10360 fn owned_terms(edge_attr_count: usize) -> Vec<EvidenceTerm> {
10364 vec![
10365 EvidenceTerm {
10366 signal: "self_loop".into(),
10367 observed_value: bool_evidence(false),
10368 log_likelihood_ratio: -0.5,
10369 },
10370 EvidenceTerm {
10371 signal: "edge_attr_count".into(),
10372 observed_value: count_evidence(edge_attr_count),
10373 log_likelihood_ratio: -2.0,
10374 },
10375 EvidenceTerm {
10376 signal: "left_autocreated".into(),
10377 observed_value: bool_evidence(true),
10378 log_likelihood_ratio: -1.25,
10379 },
10380 EvidenceTerm {
10381 signal: "right_autocreated".into(),
10382 observed_value: bool_evidence(true),
10383 log_likelihood_ratio: -1.25,
10384 },
10385 ]
10386 }
10387 fn borrowed_terms(edge_attr_count: usize) -> Vec<CowTerm> {
10388 vec![
10389 CowTerm {
10390 signal: Cow::Borrowed("self_loop"),
10391 observed_value: Cow::Borrowed("false"),
10392 log_likelihood_ratio: -0.5,
10393 },
10394 CowTerm {
10395 signal: Cow::Borrowed("edge_attr_count"),
10396 observed_value: Cow::Owned(edge_attr_count.to_string()),
10398 log_likelihood_ratio: -2.0,
10399 },
10400 CowTerm {
10401 signal: Cow::Borrowed("left_autocreated"),
10402 observed_value: Cow::Borrowed("true"),
10403 log_likelihood_ratio: -1.25,
10404 },
10405 CowTerm {
10406 signal: Cow::Borrowed("right_autocreated"),
10407 observed_value: Cow::Borrowed("true"),
10408 log_likelihood_ratio: -1.25,
10409 },
10410 ]
10411 }
10412
10413 let owned_arm = || -> Duration {
10414 let policy = RuntimePolicy::strict();
10415 let mode = policy.mode();
10416 let mut log: Vec<DecisionRecord> = Vec::new();
10417 let start = Instant::now();
10418 for index in 0..EDGES {
10419 let action = policy.action_for(0.08, false);
10420 if log.len() == CAP {
10421 log.clear();
10422 }
10423 log.push(DecisionRecord {
10424 ts_unix_ms: fnx_runtime::unix_time_ms(),
10425 operation: "add_edge".into(),
10426 mode,
10427 action,
10428 incompatibility_probability: 0.08,
10429 rationale: RATIONALE.into(),
10430 evidence: owned_terms(index & 3),
10431 });
10432 }
10433 let elapsed = start.elapsed();
10434 let mut sink = 0usize;
10439 for record in &log {
10440 sink ^= record.operation.len()
10441 ^ record.rationale.len()
10442 ^ (record.ts_unix_ms as usize)
10443 ^ (record.incompatibility_probability.to_bits() as usize)
10444 ^ usize::from(matches!(record.action, DecisionAction::Allow));
10445 for term in &record.evidence {
10446 sink ^= term.signal.len()
10447 ^ term.observed_value.len()
10448 ^ (term.log_likelihood_ratio.to_bits() as usize);
10449 }
10450 }
10451 black_box(sink);
10452 elapsed
10453 };
10454
10455 let borrowed_arm = || -> Duration {
10456 let policy = RuntimePolicy::strict();
10457 let mut log: Vec<CowRecord> = Vec::new();
10458 let start = Instant::now();
10459 for index in 0..EDGES {
10460 let action = policy.action_for(0.08, false);
10461 if log.len() == CAP {
10462 log.clear();
10463 }
10464 log.push(CowRecord {
10465 ts_unix_ms: fnx_runtime::unix_time_ms(),
10466 operation: Cow::Borrowed("add_edge"),
10467 action,
10468 incompatibility_probability: 0.08,
10469 rationale: Cow::Borrowed(RATIONALE),
10470 evidence: borrowed_terms(index & 3),
10471 });
10472 }
10473 let elapsed = start.elapsed();
10474 let mut sink = 0usize;
10475 for record in &log {
10476 sink ^= record.operation.len()
10477 ^ record.rationale.len()
10478 ^ (record.ts_unix_ms as usize)
10479 ^ (record.incompatibility_probability.to_bits() as usize)
10480 ^ usize::from(matches!(record.action, DecisionAction::Allow));
10481 for term in &record.evidence {
10482 sink ^= term.signal.len()
10483 ^ term.observed_value.len()
10484 ^ (term.log_likelihood_ratio.to_bits() as usize);
10485 }
10486 }
10487 black_box(sink);
10488 elapsed
10489 };
10490
10491 let balanced =
10496 |first: &dyn Fn() -> Duration, second: &dyn Fn() -> Duration| -> (f64, f64) {
10497 let a1 = first();
10498 let b1 = second();
10499 let b2 = second();
10500 let a2 = first();
10501 let scale = (2 * EDGES) as f64;
10502 (
10503 (a1 + a2).as_nanos() as f64 / scale,
10504 (b1 + b2).as_nanos() as f64 / scale,
10505 )
10506 };
10507
10508 owned_arm();
10509 borrowed_arm();
10510
10511 let mut owned_ns = Vec::with_capacity(ROUNDS);
10512 let mut cow_ns = Vec::with_capacity(ROUNDS);
10513 let mut null_a_ns = Vec::with_capacity(ROUNDS);
10514 let mut null_b_ns = Vec::with_capacity(ROUNDS);
10515 for _ in 0..ROUNDS {
10516 let (na, nb) = balanced(&owned_arm, &owned_arm);
10517 let (owned, cow) = balanced(&owned_arm, &borrowed_arm);
10518 null_a_ns.push(na);
10519 null_b_ns.push(nb);
10520 owned_ns.push(owned);
10521 cow_ns.push(cow);
10522 }
10523
10524 let median = |samples: &mut Vec<f64>| {
10525 samples.sort_by(f64::total_cmp);
10526 samples[samples.len() / 2]
10527 };
10528 let owned = median(&mut owned_ns);
10529 let cow = median(&mut cow_ns);
10530 let null_a = median(&mut null_a_ns);
10531 let null_b = median(&mut null_b_ns);
10532
10533 println!("ledger_cow_field_cost_ab n={EDGES} rounds={ROUNDS} cap={CAP}");
10534 println!(" S String fields : {owned:8.1} ns/edge (incumbent)");
10535 println!(" C Cow fields : {cow:8.1} ns/edge (proposed)");
10536 println!(" N1 String, null A : {null_a:8.1} ns/edge");
10537 println!(" N2 String, null B : {null_b:8.1} ns/edge");
10538 println!(" -> A/A null (N2/N1): {:8.4}x", null_b / null_a);
10539 println!(" -> Cow gain (S/C) : {:8.4}x", owned / cow);
10540 println!(" -> saving : {:8.1} ns/edge", owned - cow);
10541 }
10542
10543 #[test]
10549 #[ignore]
10550 fn ab_bench_multigraph_remove_node() {
10551 use std::time::Instant;
10552 const N: usize = 1000;
10553 const M: usize = 8000;
10554 const ITERS: usize = 500;
10555 let build = || {
10556 let mut g = MultiGraph::new(CompatibilityMode::Strict);
10557 for i in 0..N {
10558 let _ = g.add_node(i.to_string());
10559 }
10560 let mut s: u64 = 0x9E3779B97F4A7C15;
10561 let mut next = || {
10562 s = s
10563 .wrapping_mul(6364136223846793005)
10564 .wrapping_add(1442695040888963407);
10565 (s >> 33) as usize % N
10566 };
10567 for _ in 0..M {
10568 let a = next();
10569 let b = next();
10570 let _ = g.add_edge(a.to_string(), b.to_string());
10571 }
10572 g
10573 };
10574
10575 let mut gnew = build();
10576 let victims: Vec<String> = gnew
10577 .nodes_ordered()
10578 .into_iter()
10579 .take(ITERS)
10580 .map(str::to_owned)
10581 .collect();
10582 let t = Instant::now();
10583 for node in &victims {
10584 gnew.remove_node(node);
10585 }
10586 let new_t = t.elapsed();
10587
10588 let mut gold = build();
10591 let t = Instant::now();
10592 for node in &victims {
10593 gold.remove_node(node);
10594 }
10595 let null_t = t.elapsed();
10596
10597 assert_eq!(gnew.node_count(), gold.node_count());
10598 assert_eq!(gnew.edge_count(), gold.edge_count());
10599 assert_eq!(gnew.edges_ordered(), gold.edges_ordered());
10600 eprintln!(
10601 "MultiGraph remove_node A/A x{ITERS}: candidate {new_t:?}, null {null_t:?}, ratio={:.3}",
10602 null_t.as_secs_f64() / new_t.as_secs_f64()
10603 );
10604 }
10605
10606 #[test]
10607 fn remove_nodes_from_matches_repeated_removal_and_rebuilds_indices() {
10608 let edges = [
10609 ("0", "1"),
10610 ("1", "2"),
10611 ("2", "3"),
10612 ("3", "4"),
10613 ("4", "5"),
10614 ("0", "5"),
10615 ("1", "4"),
10616 ("2", "5"),
10617 ];
10618 let mut batch = Graph::strict();
10619 let mut repeated = Graph::strict();
10620 for (left, right) in edges {
10621 batch.add_edge(left, right).expect("batch edge add");
10622 repeated.add_edge(left, right).expect("repeated edge add");
10623 }
10624
10625 let victims = ["1", "3", "99", "1"];
10626 let removed = batch.remove_nodes_from(victims);
10627 for victim in victims {
10628 let _ = repeated.remove_node(victim);
10629 }
10630
10631 assert_eq!(removed, (2, 5));
10632 assert_eq!(batch.snapshot(), repeated.snapshot());
10633 let ordered_nodes = batch
10634 .nodes_ordered()
10635 .into_iter()
10636 .map(str::to_owned)
10637 .collect::<Vec<_>>();
10638 for (node_index, node) in ordered_nodes.iter().enumerate() {
10639 let names_from_strings = batch.neighbors(node).expect("node should exist");
10640 let names_from_indices = batch
10641 .neighbors_indices(node_index)
10642 .expect("index should exist")
10643 .iter()
10644 .map(|index| batch.get_node_name(*index).expect("index should resolve"))
10645 .collect::<Vec<_>>();
10646 assert_eq!(names_from_indices, names_from_strings);
10647 }
10648 let names_from_edge_indices = batch
10649 .edges_storage_order_index_iter()
10650 .map(|(left, right, _)| {
10651 (
10652 batch
10653 .get_node_name(left)
10654 .expect("left endpoint should resolve")
10655 .to_owned(),
10656 batch
10657 .get_node_name(right)
10658 .expect("right endpoint should resolve")
10659 .to_owned(),
10660 )
10661 })
10662 .collect::<Vec<_>>();
10663 let names_from_edge_storage = batch
10664 .edges_storage_order_iter()
10665 .map(|(left, right, _)| (left.to_owned(), right.to_owned()))
10666 .collect::<Vec<_>>();
10667 assert_eq!(names_from_edge_indices, names_from_edge_storage);
10668 }
10669
10670 fn assert_graph_positions_agree_with_names(g: &Graph) {
10673 let names: Vec<String> = g.nodes_ordered().into_iter().map(str::to_owned).collect();
10674 for (position, name) in names.iter().enumerate() {
10675 assert_eq!(g.get_node_index(name), Some(position));
10676 assert_eq!(g.get_node_name(position), Some(name.as_str()));
10677 let by_name = g.neighbors(name).expect("live node");
10678 let by_position: Vec<&str> = g
10679 .neighbors_indices(position)
10680 .expect("live position")
10681 .iter()
10682 .map(|&p| g.get_node_name(p).expect("neighbour position"))
10683 .collect();
10684 assert_eq!(by_position, by_name, "row of {name}");
10685 let incident: Vec<(bool, &AttrMap)> =
10687 g.incident_edge_attrs(name).expect("live node").collect();
10688 let expected: Vec<(bool, &AttrMap)> = by_name
10689 .iter()
10690 .map(|&nbr| (nbr == name, g.edge_attrs(name, nbr).expect("edge")))
10691 .collect();
10692 assert_eq!(incident, expected, "incident edges of {name}");
10693 }
10694 assert!(g.neighbors_indices(names.len()).is_none());
10695 let by_positions: Vec<(&str, &str)> = g
10696 .edges_storage_order_index_iter()
10697 .map(|(l, r, _)| (g.get_node_name(l).unwrap(), g.get_node_name(r).unwrap()))
10698 .collect();
10699 let by_storage: Vec<(&str, &str)> = g
10700 .edges_storage_order_iter()
10701 .map(|(l, r, _)| (l, r))
10702 .collect();
10703 assert_eq!(by_positions, by_storage);
10704 g.with_edge_slots_by_neighbor(|rows| {
10705 assert_eq!(rows.len(), names.len());
10706 for (position, row) in rows.iter().enumerate() {
10707 let neighbours = g.neighbors_indices(position).unwrap();
10708 assert_eq!(row.len(), neighbours.len());
10709 for (&edge_slot, &neighbour) in row.iter().zip(neighbours) {
10710 assert_eq!(
10711 g.edge_attrs_by_slot(edge_slot),
10712 g.edge_attrs(&names[position], &names[neighbour])
10713 );
10714 }
10715 }
10716 });
10717 }
10718
10719 #[test]
10725 fn removal_runs_keep_positional_readers_in_step_with_names() {
10726 let mut state = 0x9e37_79b9_7f4a_7c15_u64;
10727 let mut next = |bound: usize| {
10728 state ^= state << 13;
10729 state ^= state >> 7;
10730 state ^= state << 17;
10731 usize::try_from(state % bound as u64).unwrap()
10732 };
10733 for round in 0..40 {
10734 let n = 12 + round % 20;
10735 let mut g = Graph::strict();
10736 for i in 0..n {
10737 let _ = g.add_node(format!("n{i}"));
10738 }
10739 for _ in 0..3 * n {
10740 let (u, v) = (next(n), next(n));
10741 let mut attrs = AttrMap::new();
10742 attrs.insert(
10743 "w".into(),
10744 CgseValue::Int(i64::try_from(u * 100 + v).unwrap()),
10745 );
10746 g.add_edge_with_attrs(format!("n{u}"), format!("n{v}"), attrs)
10747 .expect("edge");
10748 }
10749 assert!(g.is_compact());
10750 for _ in 0..n / 3 {
10752 let names: Vec<String> = g.nodes_ordered().into_iter().map(str::to_owned).collect();
10753 let victim = names[next(names.len() - 1)].clone();
10754 assert!(g.remove_node(&victim));
10755 assert!(!g.is_compact(), "round {round}: slots should stay apart");
10756 assert_graph_positions_agree_with_names(&g);
10757 }
10758 g.add_edge("fresh", "n0").expect("edge");
10760 assert!(g.is_compact());
10761 assert_graph_positions_agree_with_names(&g);
10762 }
10763 }
10764
10765 #[test]
10768 fn removal_runs_compact_once_freed_slots_outnumber_live_nodes() {
10769 let mut g = Graph::strict();
10770 for i in 0..10 {
10771 g.add_edge(format!("n{i}"), format!("n{}", (i + 1) % 10))
10772 .expect("edge");
10773 }
10774 for i in 0..5 {
10775 assert!(g.remove_node(&format!("n{i}")));
10776 assert!(!g.is_compact());
10777 }
10778 assert!(g.remove_node("n5"));
10779 assert!(g.is_compact(), "6 freed slots against 4 live nodes");
10780 assert_graph_positions_agree_with_names(&g);
10781 }
10782
10783 #[test]
10786 fn node_order_positions_follow_removals_and_compaction() {
10787 use super::NodeOrder;
10788 let names: Vec<String> = (0..12).map(|i| format!("v{i}")).collect();
10789 let mut order = NodeOrder::from_names(names.clone());
10790 let mut model = names.clone();
10791 let check = |order: &NodeOrder, model: &[String]| {
10792 assert_eq!(order.len(), model.len());
10793 assert_eq!(order.keys().cloned().collect::<Vec<_>>(), model);
10794 for (position, name) in model.iter().enumerate() {
10795 assert_eq!(order.get_index_of(name), Some(position));
10796 let (at, slot) = order.get_index(position).expect("live position");
10797 assert_eq!(at, name);
10798 assert_eq!(order.slot_at(position), Some(slot));
10799 assert_eq!(order.position_of(slot), Some(position));
10800 assert_eq!(order.get(name), Some(&slot));
10801 }
10802 assert_eq!(order.get_index(model.len()), None);
10803 };
10804 for victim in ["v3", "v0", "v7", "v11", "v10"] {
10805 let (slot, popped) = order.remove(victim).expect("live name");
10806 model.retain(|name| name != victim);
10807 assert!(
10808 !popped,
10809 "{victim} is not the last live entry of a dense table"
10810 );
10811 assert_eq!(order.position_of(slot), None);
10812 assert!(order.get(victim).is_none());
10813 assert!(!order.is_dense());
10814 check(&order, &model);
10815 }
10816 assert!(order.remove("v3").is_none());
10817 order.push("late".to_owned());
10819 model.push("late".to_owned());
10820 check(&order, &model);
10821 let old_to_new = order.compact();
10823 assert!(order.is_dense());
10824 assert_eq!(order.slot_count(), model.len());
10825 assert_eq!(old_to_new[3], usize::MAX);
10826 assert_eq!(old_to_new[1], 0);
10827 check(&order, &model);
10828 let last = model.pop().expect("non-empty");
10830 assert_eq!(order.remove(&last).map(|(_, popped)| popped), Some(true));
10831 assert!(order.is_dense());
10832 check(&order, &model);
10833 }
10834
10835 #[test]
10840 fn names_are_positions_is_never_true_when_a_name_is_off_position() {
10841 use super::{NodeOrder, is_decimal_of};
10842 assert!(is_decimal_of("0", 0));
10843 assert!(is_decimal_of("10", 10));
10844 assert!(is_decimal_of(&usize::MAX.to_string(), usize::MAX));
10845 for (name, n) in [
10846 ("", 0),
10847 ("00", 0),
10848 ("01", 1),
10849 ("+1", 1),
10850 ("-1", 1),
10851 ("1 ", 1),
10852 ("11", 1),
10853 ("1", 11),
10854 ("str:1:1", 1),
10855 ] {
10856 assert!(!is_decimal_of(name, n), "{name:?} spelled {n}");
10857 }
10858 let truth = |names: &[String]| {
10859 names
10860 .iter()
10861 .enumerate()
10862 .all(|(position, name)| *name == position.to_string())
10863 };
10864 let mut state = 0x2545_f491_4f6c_dd1d_u64;
10865 let mut next = |bound: usize| {
10866 state ^= state << 13;
10867 state ^= state >> 7;
10868 state ^= state << 17;
10869 usize::try_from(state % bound as u64).unwrap()
10870 };
10871 for round in 0..200 {
10872 let mut order = NodeOrder::default();
10873 let mut model: Vec<String> = Vec::new();
10874 for step in 0..40 {
10875 match next(5) {
10876 0..=2 => {
10878 let name = if next(4) == 0 {
10879 format!("{}", next(50))
10880 } else {
10881 model.len().to_string()
10882 };
10883 if !order.contains_key(&name) {
10884 order.push(name.clone());
10885 model.push(name);
10886 }
10887 }
10888 3 if !model.is_empty() => {
10889 let victim = model.pop().expect("non-empty");
10890 order.remove(&victim);
10891 }
10892 4 if !model.is_empty() => {
10893 let victim = model.remove(next(model.len()));
10894 order.remove(&victim);
10895 }
10896 _ => {}
10897 }
10898 if step % 3 == 0 {
10899 let flag = order.names_are_positions();
10900 assert!(
10901 !flag || truth(&model),
10902 "round {round} step {step}: {model:?}"
10903 );
10904 assert_eq!(
10905 NodeOrder::from_names(model.clone()).names_are_positions(),
10906 truth(&model)
10907 );
10908 }
10909 }
10910 }
10911 }
10912
10913 #[test]
10918 fn slot_order_names_are_positions_is_never_true_when_a_name_is_off_position() {
10919 use super::SlotOrder;
10920 let truth = |names: &[String]| {
10921 names
10922 .iter()
10923 .enumerate()
10924 .all(|(position, name)| *name == position.to_string())
10925 };
10926 let mut state = 0x9e37_79b9_7f4a_7c15_u64;
10927 let mut next = |bound: usize| {
10928 state ^= state << 13;
10929 state ^= state >> 7;
10930 state ^= state << 17;
10931 usize::try_from(state % bound as u64).unwrap()
10932 };
10933 for round in 0..200 {
10934 let mut order = SlotOrder::default();
10935 let mut model: Vec<String> = Vec::new();
10936 for step in 0..40 {
10937 match next(6) {
10938 0..=2 => {
10939 let name = if next(4) == 0 {
10940 format!("{}", next(50))
10941 } else {
10942 model.len().to_string()
10943 };
10944 if !order.contains_key(&name) {
10945 order.insert(name.clone());
10946 model.push(name);
10947 }
10948 }
10949 3 if !model.is_empty() => {
10950 let victim = model.pop().expect("non-empty");
10951 order.remove(&victim);
10952 }
10953 4 if !model.is_empty() => {
10954 let victim = model.remove(next(model.len()));
10955 order.remove(&victim);
10956 }
10957 5 if model.len() > 1 => {
10958 let victim = model.remove(next(model.len()));
10959 let slot = *order.get(&victim).expect("live");
10960 order.remove_slots(&std::iter::once(slot).collect());
10961 }
10962 _ => {}
10963 }
10964 if step % 3 == 0 {
10965 let flag = order.names_are_positions();
10966 assert!(
10967 !flag || truth(&model),
10968 "round {round} step {step}: {model:?}"
10969 );
10970 let mut fresh = SlotOrder::default();
10971 for name in &model {
10972 fresh.insert(name.clone());
10973 }
10974 assert_eq!(fresh.names_are_positions(), truth(&model));
10975 }
10976 }
10977 }
10978 }
10979
10980 #[test]
10985 fn slot_order_positions_follow_reuse_removals_and_compaction() {
10986 use super::SlotOrder;
10987 let mut state = 0x2545_f491_4f6c_dd1d_u64;
10988 let mut next = |bound: usize| {
10989 state ^= state << 13;
10990 state ^= state >> 7;
10991 state ^= state << 17;
10992 usize::try_from(state % bound as u64).unwrap()
10993 };
10994 let check = |order: &SlotOrder, model: &[String]| {
10995 assert_eq!(order.len(), model.len());
10996 assert_eq!(order.keys().cloned().collect::<Vec<_>>(), model);
10997 for (position, name) in model.iter().enumerate() {
10998 assert_eq!(order.get_index_of(name), Some(position));
10999 let (at, slot) = order.get_index(position).expect("live position");
11000 assert_eq!(at, name);
11001 assert_eq!(order.slot_at(position), Some(slot));
11002 assert_eq!(order.get(name), Some(&slot));
11003 assert_eq!(order.name(slot), Some(name.as_str()));
11004 }
11005 assert_eq!(order.slot_at(model.len()), None);
11006 assert_eq!(
11007 order.slot_count(),
11008 order.len() + order.free_slots().len(),
11009 "a slot is neither live nor free"
11010 );
11011 assert!(order.entries.len() <= 2 * order.len());
11013 };
11014 let mut order = SlotOrder::default();
11015 let mut model: Vec<String> = Vec::new();
11016 let mut fresh = 0;
11017 let mut saw_tombstones = false;
11018 for step in 0..3000 {
11019 match next(10) {
11020 0..=4 => {
11021 let name = format!("v{fresh}");
11022 fresh += 1;
11023 let free_before = order.free_slots().last().copied();
11024 let (slot, grew) = order.insert(name.clone());
11025 assert_eq!(grew, free_before.is_none());
11027 if let Some(reused) = free_before {
11028 assert_eq!(slot, reused);
11029 }
11030 model.push(name);
11031 }
11032 5..=8 if !model.is_empty() => {
11033 let victim = model.remove(next(model.len()));
11034 let slot = order.remove(&victim).expect("live name");
11035 assert_eq!(order.free_slots().last(), Some(&slot));
11036 assert_eq!(order.remove(&victim), None);
11037 }
11038 9 if model.len() > 3 => {
11039 let doomed: rustc_hash::FxHashSet<usize> = (0..3)
11040 .map(|_| *order.get(&model[next(model.len())]).expect("live"))
11041 .collect();
11042 model.retain(|name| !doomed.contains(order.get(name).expect("live")));
11043 order.remove_slots(&doomed);
11044 }
11045 _ => {}
11046 }
11047 saw_tombstones |= order.entries.len() > order.len();
11048 if step % 4 != 3 {
11050 check(&order, &model);
11051 }
11052 }
11053 assert!(
11054 saw_tombstones,
11055 "the run never left a tombstone to rank past"
11056 );
11057 }
11058
11059 #[test]
11060 fn multigraph_ordered_indices_match_borrowed_edges() {
11061 let mut graph = MultiGraph::strict();
11062 graph
11063 .add_edge_with_key_and_attrs("b", "a", 2, single_attr("w", "left"))
11064 .expect("first keyed edge should be added");
11065 graph
11066 .add_edge_with_key_and_attrs("a", "b", 0, single_attr("w", "right"))
11067 .expect("parallel keyed edge should be added");
11068 graph
11069 .add_edge_with_key_and_attrs("a", "c", 3, AttrMap::new())
11070 .expect("third edge should be added");
11071 graph
11072 .add_edge_with_key_and_attrs("c", "c", 1, single_attr("w", "loop"))
11073 .expect("self-loop should be added");
11074
11075 let nodes = graph.nodes_ordered();
11076 let from_indices = graph
11077 .edges_ordered_indices_borrowed()
11078 .into_iter()
11079 .map(|(left, right, key, attrs)| {
11080 (
11081 nodes[left].to_owned(),
11082 nodes[right].to_owned(),
11083 key,
11084 attrs.clone(),
11085 )
11086 })
11087 .collect::<Vec<_>>();
11088 let from_borrowed = graph
11089 .edges_ordered_borrowed()
11090 .into_iter()
11091 .map(|(left, right, key, attrs)| {
11092 (left.to_owned(), right.to_owned(), key, attrs.clone())
11093 })
11094 .collect::<Vec<_>>();
11095
11096 assert_eq!(from_indices, from_borrowed);
11097 }
11098
11099 #[test]
11100 fn multigraph_node_index_accessors_follow_remove_readd() {
11101 let mut graph = MultiGraph::strict();
11102 for node in ["prefix", "s", "a", "b", "t"] {
11103 let _ = graph.add_node(node);
11104 }
11105
11106 for (expected_index, node) in ["prefix", "s", "a", "b", "t"].into_iter().enumerate() {
11107 assert_eq!(graph.get_node_index(node), Some(expected_index));
11108 assert_eq!(graph.get_node_name(expected_index), Some(node));
11109 }
11110
11111 assert!(graph.remove_node("prefix"));
11112 assert!(graph.remove_node("a"));
11113 let _ = graph.add_node("a");
11114
11115 for (expected_index, node) in ["s", "b", "t", "a"].into_iter().enumerate() {
11116 assert_eq!(graph.get_node_index(node), Some(expected_index));
11117 assert_eq!(graph.get_node_name(expected_index), Some(node));
11118 }
11119 assert_eq!(graph.get_node_index("prefix"), None);
11120 assert_eq!(graph.get_node_name(graph.node_count()), None);
11121 }
11122
11123 #[test]
11124 fn strict_mode_fails_closed_for_unknown_incompatible_feature() {
11125 let mut graph = Graph::strict();
11126 let mut attrs = AttrMap::new();
11127 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
11128 let err = graph
11129 .add_edge_with_attrs("a", "b", attrs)
11130 .expect_err("strict mode should fail closed");
11131
11132 assert_eq!(
11133 err,
11134 GraphError::FailClosed {
11135 operation: "add_edge",
11136 reason: "incompatible edge metadata".to_owned(),
11137 }
11138 );
11139
11140 let last_record = graph
11141 .evidence_ledger()
11142 .records()
11143 .last()
11144 .expect("strict fail-closed path should emit a ledger row");
11145 assert_decision_record_schema(last_record, CompatibilityMode::Strict);
11146 assert_eq!(last_record.operation, "add_edge");
11147 assert_eq!(last_record.action, DecisionAction::FailClosed);
11148 assert!(
11149 last_record
11150 .evidence
11151 .iter()
11152 .any(|term| term.signal == "unknown_incompatible_feature")
11153 );
11154 }
11155
11156 fn add_edge_records(graph_records: &[DecisionRecord]) -> Vec<Vec<&str>> {
11162 graph_records
11163 .iter()
11164 .filter(|record| record.operation == "add_edge")
11165 .map(|record| {
11166 record
11167 .evidence
11168 .iter()
11169 .map(|term| term.signal.as_ref())
11170 .collect()
11171 })
11172 .collect()
11173 }
11174
11175 #[test]
11176 fn allowed_add_edge_records_only_the_outcome_decision() {
11177 let mut graph = Graph::strict();
11178 graph
11179 .add_edge_with_attrs("a", "b", AttrMap::new())
11180 .expect("edge add should succeed");
11181
11182 assert_eq!(
11183 add_edge_records(graph.evidence_ledger().records()),
11184 vec![vec![
11185 "self_loop",
11186 "edge_attr_count",
11187 "left_autocreated",
11188 "right_autocreated",
11189 ]]
11190 );
11191 assert_eq!(
11192 graph
11193 .evidence_ledger()
11194 .records()
11195 .iter()
11196 .filter(|record| record.operation == "add_edge")
11197 .map(|record| record.action)
11198 .collect::<Vec<_>>(),
11199 vec![DecisionAction::Allow]
11200 );
11201 }
11202
11203 #[test]
11204 fn add_edge_autocreation_files_no_add_node_record() {
11205 let mut graph = Graph::strict();
11211 graph
11212 .add_edge_with_attrs("a", "b", AttrMap::new())
11213 .expect("edge add should succeed");
11214
11215 let records = graph.evidence_ledger().records();
11216 assert_eq!(
11217 records.len(),
11218 1,
11219 "add_edge creating two nodes must file one record, got {:?}",
11220 records
11221 .iter()
11222 .map(|record| record.operation.as_ref())
11223 .collect::<Vec<_>>()
11224 );
11225 let record = &records[0];
11226 assert_eq!(record.operation, "add_edge");
11227 assert_eq!(
11228 record
11229 .evidence
11230 .iter()
11231 .filter(
11232 |term| term.signal == "left_autocreated" || term.signal == "right_autocreated"
11233 )
11234 .map(|term| term.observed_value.as_ref())
11235 .collect::<Vec<_>>(),
11236 vec!["true", "true"],
11237 "the autocreation event must still be auditable from the outcome record"
11238 );
11239
11240 let before = graph.evidence_ledger().records().len();
11242 graph.add_node("c");
11243 let records = graph.evidence_ledger().records();
11244 assert_eq!(records.len(), before + 1);
11245 assert_eq!(
11246 records.last().map(|record| record.operation.as_ref()),
11247 Some("add_node")
11248 );
11249 }
11250
11251 #[test]
11258 fn multigraph_slot_resolution_preserves_key_assignment() {
11259 let mut graph = MultiGraph::strict();
11260
11261 assert_eq!(
11263 graph
11264 .add_edge_with_attrs("a", "b", AttrMap::new())
11265 .expect("edge is allowed"),
11266 0
11267 );
11268 assert_eq!(
11269 graph
11270 .add_edge_with_attrs("a", "b", AttrMap::new())
11271 .expect("edge is allowed"),
11272 1
11273 );
11274 assert_eq!(
11275 graph
11276 .add_edge_with_attrs("b", "a", AttrMap::new())
11277 .expect("reversed endpoints are the same undirected pair"),
11278 2
11279 );
11280
11281 assert_eq!(
11283 graph
11284 .add_edge_with_attrs("a", "a", AttrMap::new())
11285 .expect("self-loop is allowed"),
11286 0
11287 );
11288 assert_eq!(
11289 graph
11290 .add_edge_with_attrs("a", "a", AttrMap::new())
11291 .expect("parallel self-loop is allowed"),
11292 1
11293 );
11294
11295 assert_eq!(
11297 graph
11298 .add_edge_with_key_and_attrs("a", "b", 9, AttrMap::new())
11299 .expect("explicit key is allowed"),
11300 9
11301 );
11302 assert_eq!(
11303 graph
11304 .add_edge_with_key_and_attrs("a", "a", 9, AttrMap::new())
11305 .expect("explicit key is allowed"),
11306 9
11307 );
11308
11309 assert_eq!(graph.node_count(), 2);
11310 assert_eq!(graph.edge_count(), 7);
11311 }
11312
11313 #[test]
11320 fn add_edge_fresh_bucket_take_matches_merge_semantics() {
11321 fn weight(value: i64) -> AttrMap {
11322 let mut attrs = AttrMap::new();
11323 attrs.insert("w".to_owned(), CgseValue::Int(value));
11324 attrs
11325 }
11326 fn stored(graph: &Graph, left: &str, right: &str) -> Vec<(String, CgseValue)> {
11327 graph
11328 .edge_attrs(left, right)
11329 .expect("edge exists")
11330 .iter()
11331 .map(|(key, value)| (key.clone(), value.clone()))
11332 .collect()
11333 }
11334
11335 let mut graph = Graph::strict();
11337 graph
11338 .add_edge_with_attrs("a", "b", weight(1))
11339 .expect("edge is allowed");
11340 assert_eq!(
11341 stored(&graph, "a", "b"),
11342 vec![("w".to_owned(), CgseValue::Int(1))]
11343 );
11344
11345 graph
11347 .add_edge_with_attrs("c", "d", AttrMap::new())
11348 .expect("edge is allowed");
11349 assert!(stored(&graph, "c", "d").is_empty());
11350
11351 graph
11354 .add_edge_with_attrs("c", "d", weight(7))
11355 .expect("edge is allowed");
11356 assert_eq!(
11357 stored(&graph, "c", "d"),
11358 vec![("w".to_owned(), CgseValue::Int(7))]
11359 );
11360
11361 graph
11364 .add_edge_with_attrs("a", "b", weight(9))
11365 .expect("edge is allowed");
11366 let mut extra = AttrMap::new();
11367 extra.insert("k".to_owned(), CgseValue::Int(2));
11368 graph
11369 .add_edge_with_attrs("a", "b", extra)
11370 .expect("edge is allowed");
11371 assert_eq!(
11374 stored(&graph, "a", "b"),
11375 vec![
11376 ("w".to_owned(), CgseValue::Int(9)),
11377 ("k".to_owned(), CgseValue::Int(2)),
11378 ]
11379 );
11380 }
11381
11382 #[test]
11383 fn add_edge_entry_probe_keeps_existing_empty_edge_structurally_unchanged() {
11384 let mut graph = Graph::strict();
11385 graph
11386 .add_edge_with_attrs("a", "b", AttrMap::new())
11387 .expect("initial edge is allowed");
11388 let revision_before = graph.revision;
11389
11390 graph
11391 .add_edge_with_attrs("b", "a", AttrMap::new())
11392 .expect("duplicate edge is allowed");
11393
11394 assert_eq!(graph.edge_count(), 1);
11395 assert_eq!(graph.neighbors("a"), Some(vec!["b"]));
11396 assert_eq!(graph.neighbors("b"), Some(vec!["a"]));
11397 assert_eq!(graph.revision, revision_before);
11398 }
11399
11400 #[test]
11412 fn add_edge_endpoint_resolution_preserves_autocreation_and_order() {
11413 fn autocreated(graph: &Graph) -> (String, String) {
11414 let record = graph
11415 .evidence_ledger()
11416 .records()
11417 .last()
11418 .expect("add_edge files an outcome record");
11419 let read = |signal: &str| {
11420 record
11421 .evidence
11422 .iter()
11423 .find(|term| term.signal == signal)
11424 .map(|term| term.observed_value.to_string())
11425 .unwrap_or_else(|| panic!("{signal} must be present"))
11426 };
11427 (read("left_autocreated"), read("right_autocreated"))
11428 }
11429
11430 let mut graph = Graph::strict();
11432 graph
11433 .add_edge_with_attrs("s", "s", AttrMap::new())
11434 .expect("self-loop is allowed");
11435 assert_eq!(autocreated(&graph), ("true".to_owned(), "true".to_owned()));
11436 assert_eq!(graph.node_count(), 1);
11437
11438 graph
11440 .add_edge_with_attrs("s", "s", AttrMap::new())
11441 .expect("re-adding a self-loop is allowed");
11442 assert_eq!(
11443 autocreated(&graph),
11444 ("false".to_owned(), "false".to_owned())
11445 );
11446 assert_eq!(graph.node_count(), 1);
11447
11448 graph
11450 .add_edge_with_attrs("s", "t", AttrMap::new())
11451 .expect("edge is allowed");
11452 assert_eq!(autocreated(&graph), ("false".to_owned(), "true".to_owned()));
11453
11454 graph
11457 .add_edge_with_attrs("z", "a", AttrMap::new())
11458 .expect("edge is allowed");
11459 assert_eq!(autocreated(&graph), ("true".to_owned(), "true".to_owned()));
11460 assert_eq!(graph.get_node_index("s"), Some(0));
11461 assert_eq!(graph.get_node_index("t"), Some(1));
11462 assert_eq!(graph.get_node_index("z"), Some(2));
11463 assert_eq!(graph.get_node_index("a"), Some(3));
11464 }
11465
11466 #[test]
11473 fn mutation_ledger_constants_are_borrowed_not_allocated() {
11474 use std::borrow::Cow;
11475 use std::collections::BTreeMap;
11476
11477 let mut graph = Graph::strict();
11478 let mut attrs = AttrMap::new();
11479 attrs.insert("weight".to_owned(), CgseValue::Int(3));
11480 graph
11481 .add_edge_with_attrs("a", "b", attrs)
11482 .expect("strict add_edge with an int attribute is allowed");
11483 graph.add_node("c");
11484
11485 let records = graph.evidence_ledger().records();
11486 assert_eq!(
11487 records
11488 .iter()
11489 .map(|record| record.operation.as_ref())
11490 .collect::<Vec<_>>(),
11491 vec!["add_edge", "add_node"],
11492 "ledger shape must be unchanged by the Cow migration"
11493 );
11494
11495 let expected_borrowed: BTreeMap<&str, &str> = [
11498 ("self_loop", "false"),
11499 ("left_autocreated", "true"),
11500 ("right_autocreated", "true"),
11501 ("node_preexisting", "false"),
11502 ]
11503 .into_iter()
11504 .collect();
11505
11506 for record in records {
11507 assert!(
11508 matches!(record.operation, Cow::Borrowed(_)),
11509 "operation `{}` must be borrowed",
11510 record.operation
11511 );
11512 assert!(
11513 matches!(record.rationale, Cow::Borrowed(_)),
11514 "rationale `{}` must be borrowed",
11515 record.rationale
11516 );
11517 for term in &record.evidence {
11518 assert!(
11519 matches!(term.signal, Cow::Borrowed(_)),
11520 "signal `{}` must be borrowed",
11521 term.signal
11522 );
11523 if let Some(expected) = expected_borrowed.get(term.signal.as_ref()) {
11524 assert_eq!(&term.observed_value, expected);
11525 assert!(
11526 matches!(term.observed_value, Cow::Borrowed(_)),
11527 "boolean observed_value for `{}` must be borrowed",
11528 term.signal
11529 );
11530 }
11531 }
11532 }
11533
11534 let edge_record = &records[0];
11536 assert_eq!(
11537 edge_record
11538 .evidence
11539 .iter()
11540 .find(|term| term.signal == "edge_attr_count")
11541 .map(|term| term.observed_value.as_ref()),
11542 Some("1")
11543 );
11544 }
11545
11546 #[test]
11547 fn allowed_multigraph_add_edge_records_only_the_outcome_decision() {
11548 let mut graph = MultiGraph::strict();
11549 let _ = graph
11550 .add_edge_with_attrs("a", "b", AttrMap::new())
11551 .expect("edge add should succeed");
11552
11553 assert_eq!(
11554 add_edge_records(graph.evidence_ledger().records()),
11555 vec![vec![
11556 "edge_key",
11557 "edge_attr_count",
11558 "left_autocreated",
11559 "right_autocreated",
11560 ]]
11561 );
11562 }
11563
11564 #[test]
11565 fn refused_multigraph_add_edge_still_records_its_gate_decision() {
11566 let mut graph = MultiGraph::strict();
11570 let mut attrs = AttrMap::new();
11571 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
11572 let err = graph
11573 .add_edge_with_attrs("a", "b", attrs)
11574 .expect_err("strict mode should fail closed");
11575 assert_eq!(
11576 err,
11577 GraphError::FailClosed {
11578 operation: "add_edge",
11579 reason: "incompatible edge metadata".to_owned(),
11580 }
11581 );
11582
11583 assert_eq!(
11584 add_edge_records(graph.evidence_ledger().records()),
11585 vec![vec!["unknown_incompatible_feature"]]
11586 );
11587 assert_eq!(
11588 graph
11589 .evidence_ledger()
11590 .records()
11591 .last()
11592 .map(|record| record.action),
11593 Some(DecisionAction::FailClosed)
11594 );
11595 }
11596
11597 #[test]
11598 fn revision_increments_on_mutating_operations() {
11599 let mut graph = Graph::strict();
11600 let r0 = graph.revision();
11601 let _ = graph.add_node("a");
11602 let r1 = graph.revision();
11603 assert!(r1 > r0);
11604
11605 graph.add_edge("a", "b").expect("edge add should succeed");
11606 let r2 = graph.revision();
11607 assert!(r2 > r1);
11608
11609 let _ = graph.remove_edge("a", "b");
11610 let r3 = graph.revision();
11611 assert!(r3 > r2);
11612 }
11613
11614 #[test]
11615 fn add_node_with_attrs_reports_change_on_existing_node() {
11616 let mut graph = Graph::strict();
11617 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
11618 let r1 = graph.revision();
11619 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
11620 let r2 = graph.revision();
11621 assert!(r2 > r1);
11622 let expected = CgseValue::from("blue");
11623 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
11624 }
11625
11626 #[test]
11627 fn add_node_with_identical_attrs_keeps_order_and_revision() {
11628 let mut graph = Graph::strict();
11629 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
11630 assert!(graph.add_node("b"));
11631 let revision_before = graph.revision();
11632
11633 assert!(!graph.add_node_with_attrs("a", single_attr("color", "red")));
11634
11635 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
11636 assert_eq!(graph.revision(), revision_before);
11637 assert_eq!(
11638 graph.node_attrs("a").and_then(|attrs| attrs.get("color")),
11639 Some(&CgseValue::from("red"))
11640 );
11641 }
11642
11643 #[test]
11644 fn add_node_with_attrs_keeps_fresh_node_order_and_attrs() {
11645 let mut graph = Graph::strict();
11646 assert!(graph.add_node("before"));
11647 assert!(graph.add_node_with_attrs("fresh", single_attr("color", "red")));
11648 assert!(graph.add_node("after"));
11649
11650 assert_eq!(graph.nodes_ordered(), vec!["before", "fresh", "after"]);
11651 assert_eq!(
11652 graph
11653 .node_attrs("fresh")
11654 .and_then(|attrs| attrs.get("color")),
11655 Some(&CgseValue::from("red"))
11656 );
11657 }
11658
11659 #[test]
11660 fn multigraph_add_node_with_attrs_reports_change_on_existing_node() {
11661 let mut graph = MultiGraph::strict();
11662 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
11663 let r1 = graph.revision();
11664 assert!(graph.add_node_with_attrs("a", single_attr("color", "blue")));
11665 let r2 = graph.revision();
11666 assert!(r2 > r1);
11667 let expected = CgseValue::from("blue");
11668 assert_eq!(graph.node_attrs("a").unwrap().get("color"), Some(&expected));
11669 }
11670
11671 #[test]
11672 fn hardened_self_loop_records_allow_decision() {
11673 let mut graph = Graph::hardened();
11674 graph
11675 .add_edge("loop", "loop")
11676 .expect("hardened self-loop edge should be accepted");
11677
11678 let add_edge_record = graph
11679 .evidence_ledger()
11680 .records()
11681 .iter()
11682 .rev()
11683 .find(|record| record.operation == "add_edge")
11684 .expect("add_edge operation should emit ledger row");
11685 assert_decision_record_schema(add_edge_record, CompatibilityMode::Hardened);
11686 assert_eq!(add_edge_record.action, DecisionAction::Allow);
11687 assert!(
11688 add_edge_record
11689 .evidence
11690 .iter()
11691 .any(|term| term.signal == "self_loop" && term.observed_value == "true")
11692 );
11693 }
11694
11695 #[test]
11696 fn runtime_policy_tracks_hardened_graph_recovery() {
11697 let mut graph = Graph::hardened();
11698 graph
11699 .add_edge("a", "b")
11700 .expect("hardened graph edge add should succeed");
11701
11702 assert_eq!(graph.runtime_policy().mode(), CompatibilityMode::Hardened);
11703 assert!(!graph.runtime_policy().decision_log().records().is_empty());
11704 assert!(graph.runtime_policy().posterior().observation_count >= 1);
11705 }
11706
11707 #[test]
11708 fn snapshot_roundtrip_replays_to_identical_state() {
11709 let mut graph = Graph::strict();
11710
11711 let mut first_attrs = AttrMap::new();
11712 first_attrs.insert("weight".to_owned(), "7".into());
11713 graph
11714 .add_edge_with_attrs("a", "b", first_attrs)
11715 .expect("edge insert should succeed");
11716
11717 let mut second_attrs = AttrMap::new();
11718 second_attrs.insert("color".to_owned(), "green".into());
11719 graph
11720 .add_edge_with_attrs("b", "c", second_attrs)
11721 .expect("edge insert should succeed");
11722
11723 let snapshot = graph.snapshot();
11724 let mut replayed = Graph::new(snapshot.mode);
11725 for node in &snapshot.nodes {
11726 let _ = replayed.add_node(node.clone());
11727 }
11728 for edge in &snapshot.edges {
11729 replayed
11730 .add_edge_with_attrs(edge.left.clone(), edge.right.clone(), edge.attrs.clone())
11731 .expect("snapshot replay should be valid");
11732 }
11733
11734 assert_eq!(replayed.snapshot(), snapshot);
11735 assert_graph_core_invariants(&replayed);
11736 }
11737
11738 #[test]
11739 fn multigraph_tracks_parallel_edges_with_distinct_keys() {
11740 let mut graph = MultiGraph::strict();
11741 let first = graph.add_edge("a", "b").expect("edge add should succeed");
11742 let second = graph.add_edge("a", "b").expect("edge add should succeed");
11743
11744 assert_ne!(first, second);
11745 assert_eq!(graph.edge_count(), 2);
11746 assert_eq!(graph.edge_keys("a", "b"), Some(vec![0, 1]));
11747 assert_multigraph_core_invariants(&graph);
11748 }
11749
11750 #[test]
11751 fn multigraph_counts_parallel_selfloops_only() {
11752 let mut graph = MultiGraph::strict();
11753 let _ = graph
11754 .add_edge("a", "a")
11755 .expect("self-loop add should succeed");
11756 let _ = graph
11757 .add_edge("a", "a")
11758 .expect("self-loop add should succeed");
11759 let _ = graph
11760 .add_edge("b", "b")
11761 .expect("self-loop add should succeed");
11762 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
11763 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
11764
11765 assert_eq!(graph.number_of_selfloops(), 3);
11766 assert_multigraph_core_invariants(&graph);
11767 }
11768
11769 #[test]
11770 fn multigraph_add_fresh_edge_unrecorded_rejects_occupied_bucket() {
11771 let mut graph = MultiGraph::strict();
11772 assert_eq!(graph.add_fresh_edge_unrecorded("a", "b"), Some(0));
11773 let revision = graph.revision();
11774 let edge_count = graph.edge_count();
11775
11776 assert_eq!(graph.add_fresh_edge_unrecorded("b", "a"), None);
11777
11778 assert_eq!(graph.revision(), revision);
11779 assert_eq!(graph.edge_count(), edge_count);
11780 assert_eq!(graph.edge_keys("a", "b"), Some(vec![0]));
11781 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
11782 assert_multigraph_core_invariants(&graph);
11783 }
11784
11785 #[test]
11786 fn multigraph_add_fresh_edge_with_key_unrecorded_preserves_key() {
11787 let mut graph = MultiGraph::strict();
11788
11789 assert_eq!(
11790 graph.add_fresh_edge_with_key_unrecorded("a", "b", 42),
11791 Some(42)
11792 );
11793
11794 assert_eq!(graph.edge_count(), 1);
11795 assert_eq!(graph.edge_keys("a", "b"), Some(vec![42]));
11796 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
11797 assert_multigraph_core_invariants(&graph);
11798 }
11799
11800 #[test]
11801 fn multigraph_remove_edge_without_key_removes_latest_instance() {
11802 let mut graph = MultiGraph::strict();
11803 let first = graph.add_edge("a", "b").expect("edge add should succeed");
11804 let second = graph.add_edge("a", "b").expect("edge add should succeed");
11805
11806 assert!(graph.remove_edge("a", "b", None));
11807 assert_eq!(graph.edge_count(), 1);
11808 assert_eq!(graph.edge_keys("a", "b"), Some(vec![first]));
11809 assert_ne!(first, second);
11810 assert_multigraph_core_invariants(&graph);
11811 }
11812
11813 #[test]
11814 fn multigraph_remove_node_clears_parallel_incident_edges() {
11815 let mut graph = MultiGraph::strict();
11816 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
11817 let _ = graph.add_edge("a", "b").expect("edge add should succeed");
11818 let _ = graph.add_edge("b", "c").expect("edge add should succeed");
11819
11820 assert!(graph.remove_node("b"));
11821 assert_eq!(graph.edge_count(), 0);
11822 assert!(!graph.has_node("b"));
11823 assert_eq!(graph.neighbors("a"), Some(vec![]));
11824 assert_eq!(graph.neighbors("c"), Some(vec![]));
11825 assert_multigraph_core_invariants(&graph);
11826 }
11827
11828 #[test]
11829 fn multigraph_clear_edges_preserves_nodes_attrs_and_rows() {
11830 let mut graph = MultiGraph::strict();
11831 assert!(graph.add_node_with_attrs("a", single_attr("color", "red")));
11832 assert!(graph.add_node_with_attrs("b", single_attr("color", "blue")));
11833 let _ = graph
11834 .add_edge_with_key_and_attrs("a", "b", 7, single_attr("weight", "3"))
11835 .expect("edge add should succeed");
11836 let _ = graph
11837 .add_edge_with_key_and_attrs("b", "a", 2, single_attr("weight", "5"))
11838 .expect("edge add should succeed");
11839 let before_revision = graph.revision();
11840
11841 graph.clear_edges();
11842
11843 assert_eq!(graph.node_count(), 2);
11844 assert_eq!(graph.nodes_ordered(), vec!["a", "b"]);
11845 assert_eq!(graph.edge_count(), 0);
11846 assert!(graph.edges_ordered().is_empty());
11847 assert_eq!(graph.neighbors("a"), Some(vec![]));
11848 assert_eq!(graph.neighbors("b"), Some(vec![]));
11849 assert_eq!(
11850 graph.node_attrs("a").unwrap().get("color"),
11851 Some(&CgseValue::from("red"))
11852 );
11853 assert!(graph.revision() > before_revision);
11854 assert_multigraph_core_invariants(&graph);
11855 }
11856
11857 #[test]
11858 fn runtime_policy_tracks_hardened_multigraph_recovery() {
11859 let mut graph = MultiGraph::hardened();
11860 graph
11861 .add_edge("a", "b")
11862 .expect("hardened multigraph edge add should succeed");
11863
11864 assert_eq!(graph.runtime_policy().mode(), CompatibilityMode::Hardened);
11865 assert!(!graph.runtime_policy().decision_log().records().is_empty());
11866 assert!(graph.runtime_policy().posterior().observation_count >= 1);
11867 }
11868
11869 #[test]
11870 fn multigraph_roundtrips_sparse_snapshot_keys() {
11871 let mut graph = MultiGraph::strict();
11872 assert_eq!(
11873 graph.add_edge("a", "b").expect("edge add should succeed"),
11874 0
11875 );
11876 assert_eq!(
11877 graph.add_edge("a", "b").expect("edge add should succeed"),
11878 1
11879 );
11880 assert_eq!(
11881 graph.add_edge("a", "b").expect("edge add should succeed"),
11882 2
11883 );
11884 assert!(graph.remove_edge("a", "b", Some(1)));
11885 assert_eq!(
11886 graph.add_edge("a", "b").expect("edge add should succeed"),
11887 3
11888 );
11889
11890 let snapshot = graph.snapshot();
11891 assert_eq!(
11892 snapshot
11893 .edges
11894 .iter()
11895 .map(|edge| edge.key)
11896 .collect::<Vec<_>>(),
11897 vec![0, 2, 3]
11898 );
11899
11900 let mut replayed = MultiGraph::new(snapshot.mode);
11901 for node in &snapshot.nodes {
11902 let _ = replayed.add_node(node.clone());
11903 }
11904 for edge in &snapshot.edges {
11905 replayed
11906 .add_edge_with_key_and_attrs(
11907 edge.left.clone(),
11908 edge.right.clone(),
11909 edge.key,
11910 edge.attrs.clone(),
11911 )
11912 .expect("snapshot replay should preserve explicit keys");
11913 }
11914
11915 assert_eq!(replayed.snapshot(), snapshot);
11916 assert_multigraph_core_invariants(&replayed);
11917 }
11918
11919 proptest! {
11920 #[test]
11921 fn prop_core_invariants_hold_for_mixed_edge_mutations(
11922 ops in prop::collection::vec((0_u8..8, 0_u8..8, any::<bool>()), 1..80),
11923 ) {
11924 let mut graph = Graph::strict();
11925 let mut last_revision = graph.revision();
11926
11927 for (left_id, right_id, is_add) in ops {
11928 let left = node_name(left_id);
11929 let right = node_name(right_id);
11930 if is_add {
11931 prop_assert!(graph.add_edge(left.clone(), right.clone()).is_ok());
11932 } else {
11933 let _ = graph.remove_edge(&left, &right);
11934 }
11935 let revision = graph.revision();
11936 prop_assert!(revision >= last_revision);
11937 last_revision = revision;
11938 assert_graph_core_invariants(&graph);
11939 }
11940 }
11941
11942 #[test]
11943 fn prop_snapshot_is_deterministic_for_same_operation_stream(
11944 ops in prop::collection::vec((0_u8..8, 0_u8..8, 0_u8..3), 0..64),
11945 ) {
11946 let mut graph_left = Graph::hardened();
11947 let mut graph_right = Graph::hardened();
11948
11949 for (left_id, right_id, attrs_variant) in ops {
11950 let left = node_name(left_id);
11951 let right = node_name(right_id);
11952 let mut attrs = AttrMap::new();
11953 if attrs_variant == 1 {
11954 attrs.insert("weight".to_owned(), (left_id % 5).to_string().into());
11955 } else if attrs_variant == 2 {
11956 attrs.insert("tag".to_owned(), format!("k{}", right_id % 4).into());
11957 }
11958 prop_assert!(
11959 graph_left
11960 .add_edge_with_attrs(left.clone(), right.clone(), attrs.clone())
11961 .is_ok()
11962 );
11963 prop_assert!(
11964 graph_right
11965 .add_edge_with_attrs(left, right, attrs)
11966 .is_ok()
11967 );
11968 }
11969
11970 prop_assert_eq!(graph_left.snapshot(), graph_right.snapshot());
11971 prop_assert_eq!(graph_left.snapshot(), graph_left.snapshot());
11972 }
11973
11974 #[test]
11975 fn prop_reapplying_identical_edge_attrs_is_revision_stable(
11976 left_id in 0_u8..8,
11977 right_id in 0_u8..8,
11978 weight in 0_u16..5000,
11979 ) {
11980 let mut graph = Graph::strict();
11981 let left = node_name(left_id);
11982 let right = node_name(right_id);
11983 let mut attrs = AttrMap::new();
11984 attrs.insert("weight".to_owned(), weight.to_string().into());
11985
11986 prop_assert!(
11987 graph
11988 .add_edge_with_attrs(left.clone(), right.clone(), attrs.clone())
11989 .is_ok()
11990 );
11991 let revision_after_first = graph.revision();
11992 prop_assert!(
11993 graph
11994 .add_edge_with_attrs(left, right, attrs)
11995 .is_ok()
11996 );
11997 prop_assert_eq!(graph.revision(), revision_after_first);
11998 }
11999
12000 #[test]
12001 fn prop_remove_node_clears_incident_edges(
12002 ops in prop::collection::vec((0_u8..8, 0_u8..8), 1..64),
12003 target_id in 0_u8..8,
12004 ) {
12005 let mut graph = Graph::strict();
12006 for (left_id, right_id) in ops {
12007 let left = node_name(left_id);
12008 let right = node_name(right_id);
12009 prop_assert!(graph.add_edge(left, right).is_ok());
12010 }
12011
12012 let target = node_name(target_id);
12013 let removed = graph.remove_node(&target);
12014 if removed {
12015 prop_assert!(!graph.has_node(&target));
12016 for node in graph.nodes_ordered() {
12017 let neighbors = graph
12018 .neighbors(node)
12019 .expect("graph nodes should always have adjacency buckets");
12020 prop_assert!(!neighbors.contains(&target.as_str()));
12021 prop_assert!(!graph.has_edge(node, &target));
12022 }
12023 }
12024 assert_graph_core_invariants(&graph);
12025 }
12026
12027 #[test]
12028 fn prop_decision_ledger_records_follow_schema(
12029 ops in prop::collection::vec((0_u8..8, 0_u8..8, 0_u8..4), 1..72),
12030 ) {
12031 let mut graph = Graph::strict();
12032 for (left_id, right_id, attrs_kind) in ops {
12033 let left = node_name(left_id);
12034 let right = node_name(right_id);
12035 let mut attrs = AttrMap::new();
12036 match attrs_kind {
12037 0 => {}
12038 1 => {
12039 attrs.insert("weight".to_owned(), (left_id % 9).to_string().into());
12040 }
12041 2 => {
12042 attrs.insert("color".to_owned(), format!("c{}", right_id % 6).into());
12043 }
12044 _ => {
12045 attrs.insert("__fnx_incompatible_decoder".to_owned(), "v2".into());
12046 }
12047 }
12048 let _ = graph.add_edge_with_attrs(left, right, attrs);
12049 }
12050
12051 let records = graph.evidence_ledger().records();
12052 prop_assert!(!records.is_empty());
12053 for record in records {
12054 assert_decision_record_schema(record, CompatibilityMode::Strict);
12055 if record.operation == "add_node" {
12056 prop_assert_eq!(record.action, DecisionAction::Allow);
12057 prop_assert!(record.evidence.iter().any(|term| term.signal == "node_preexisting"));
12058 prop_assert!(record.evidence.iter().any(|term| term.signal == "attrs_count"));
12059 } else {
12060 prop_assert_eq!(&record.operation, "add_edge");
12061 if record.action == DecisionAction::FailClosed {
12062 prop_assert!(
12063 record
12064 .evidence
12065 .iter()
12066 .any(|term| term.signal == "unknown_incompatible_feature")
12067 );
12068 } else {
12069 prop_assert_eq!(record.action, DecisionAction::Allow);
12070 let is_precheck = record
12074 .evidence
12075 .iter()
12076 .any(|term| term.signal == "unknown_incompatible_feature")
12077 && !record
12078 .evidence
12079 .iter()
12080 .any(|term| term.signal == "edge_attr_count");
12081 if !is_precheck {
12082 prop_assert!(record.evidence.iter().any(|term| term.signal == "edge_attr_count"));
12083 prop_assert!(record.evidence.iter().any(|term| term.signal == "self_loop"));
12084 }
12085 }
12086 }
12087 }
12088 }
12089 }
12090}
12091
12092#[test]
12116#[ignore = "measurement; run with --release --ignored --nocapture"]
12117fn node_decision_ledger_self_time_ab() {
12118 use std::hint::black_box;
12119 use std::time::{Duration, Instant};
12120
12121 const NODES: usize = 2000;
12122 const REPS: usize = 200;
12123 const ROUNDS: usize = 41;
12124
12125 let keys: Vec<String> = (0..NODES).map(|n| format!("n{n}")).collect();
12126 let mut graph = Graph::strict();
12127 for key in &keys {
12128 let _ = graph.add_node_with_attrs(key.clone(), AttrMap::new());
12129 }
12130 let before = graph.node_count();
12131
12132 let recorded = |g: &mut Graph| -> Duration {
12135 let start = Instant::now();
12136 for _ in 0..REPS {
12137 for key in &keys {
12138 black_box(g.add_node_with_attrs(key.as_str(), AttrMap::new()));
12139 }
12140 }
12141 start.elapsed()
12142 };
12143 let unrecorded = |g: &mut Graph| -> Duration {
12144 let start = Instant::now();
12145 for _ in 0..REPS {
12146 for key in &keys {
12147 black_box(g.add_node_with_attrs_unrecorded(key.as_str(), AttrMap::new()));
12148 }
12149 }
12150 start.elapsed()
12151 };
12152
12153 let balanced = |first: &dyn Fn(&mut Graph) -> Duration,
12156 second: &dyn Fn(&mut Graph) -> Duration,
12157 g: &mut Graph|
12158 -> (f64, f64) {
12159 let a1 = first(g);
12160 let b1 = second(g);
12161 let b2 = second(g);
12162 let a2 = first(g);
12163 let scale = (2 * NODES * REPS) as f64;
12164 (
12165 (a1 + a2).as_nanos() as f64 / scale,
12166 (b1 + b2).as_nanos() as f64 / scale,
12167 )
12168 };
12169
12170 for _ in 0..3 {
12171 recorded(&mut graph);
12172 unrecorded(&mut graph);
12173 }
12174
12175 let mut rec_ns = Vec::with_capacity(ROUNDS);
12176 let mut unrec_ns = Vec::with_capacity(ROUNDS);
12177 let mut null_a = Vec::with_capacity(ROUNDS);
12178 let mut null_b = Vec::with_capacity(ROUNDS);
12179 for _ in 0..ROUNDS {
12180 let (na, nb) = balanced(&recorded, &recorded, &mut graph);
12181 null_a.push(na);
12182 null_b.push(nb);
12183 let (r, u) = balanced(&recorded, &unrecorded, &mut graph);
12184 rec_ns.push(r);
12185 unrec_ns.push(u);
12186 }
12187
12188 assert_eq!(
12189 graph.node_count(),
12190 before,
12191 "a no-op control that changed the node count measured a real insert"
12192 );
12193
12194 let median = |v: &mut Vec<f64>| -> f64 {
12195 v.sort_by(f64::total_cmp);
12196 v[(v.len() - 1) / 2]
12197 };
12198 let rec = median(&mut rec_ns);
12199 let unrec = median(&mut unrec_ns);
12200 let na = median(&mut null_a);
12201 let nb = median(&mut null_b);
12202
12203 println!("node_decision_ledger_self_time_ab nodes={NODES} reps={REPS} rounds={ROUNDS}");
12204 println!(" A/A null (recorded/recorded) : {:8.4}x", nb / na);
12205 println!(" A add_node_with_attrs : {rec:8.1} ns/call (store + ledger)");
12206 println!(" B ..._unrecorded : {unrec:8.1} ns/call (store only)");
12207 println!(
12208 " -> decision ledger costs : {:8.1} ns/call",
12209 rec - unrec
12210 );
12211 println!(
12212 " -> ledger share of the pair : {:8.1}%",
12213 100.0 * (rec - unrec) / rec
12214 );
12215}