Skip to main content

formualizer_eval/engine/
delta_edges.rs

1use super::addr::VertexAddr;
2use super::csr_edges::CsrEdges;
3use super::vertex::VertexId;
4use rustc_hash::{FxHashMap, FxHashSet};
5
6#[cfg(test)]
7mod tests {
8    use super::super::addr::GridAddr;
9
10    fn grid(row: u32, col: u32) -> VertexAddr {
11        VertexAddr::grid(GridAddr::new(row, col))
12    }
13
14    use super::*;
15
16    #[test]
17    fn test_delta_slab_add_edge() {
18        let csr = CsrEdges::from_adjacency(
19            vec![(0u32, vec![1u32])],
20            &[grid(0, 0), grid(0, 1), grid(0, 2)],
21        );
22        let mut delta = DeltaEdgeSlab::new();
23
24        delta.add_edge(VertexId(0), VertexId(2));
25
26        let merged = delta.merged_view(&csr, VertexId(0));
27        assert_eq!(merged, vec![VertexId(1), VertexId(2)]);
28    }
29
30    #[test]
31    fn test_delta_slab_remove_edge() {
32        let csr = CsrEdges::from_adjacency(
33            vec![(0u32, vec![1u32, 2u32, 3u32])],
34            &[grid(0, 0), grid(0, 1), grid(0, 2), grid(0, 3)],
35        );
36        let mut delta = DeltaEdgeSlab::new();
37
38        delta.remove_edge(VertexId(0), VertexId(2));
39
40        let merged = delta.merged_view(&csr, VertexId(0));
41        assert_eq!(merged, vec![VertexId(1), VertexId(3)]);
42    }
43
44    #[test]
45    fn test_delta_slab_rebuild_threshold() {
46        let mut edges = CsrMutableEdges::new();
47
48        // Add 1000 edges through delta slab
49        for i in 0..1000 {
50            edges.add_edge(VertexId(i), VertexId(i + 1));
51        }
52
53        // Should trigger rebuild at threshold
54        assert!(edges.delta_size() < 100); // Delta cleared after rebuild
55    }
56
57    #[test]
58    fn test_delta_slab_multiple_operations() {
59        let csr = CsrEdges::from_adjacency(
60            vec![(0u32, vec![1u32, 2u32]), (1u32, vec![3u32])],
61            &[grid(0, 0), grid(0, 1), grid(0, 2), grid(1, 0)],
62        );
63        let mut delta = DeltaEdgeSlab::new();
64
65        // Multiple operations on same vertex
66        delta.add_edge(VertexId(0), VertexId(3));
67        delta.remove_edge(VertexId(0), VertexId(1));
68        delta.add_edge(VertexId(0), VertexId(4));
69
70        let merged = delta.merged_view(&csr, VertexId(0));
71        assert_eq!(merged, vec![VertexId(2), VertexId(3), VertexId(4)]);
72    }
73
74    #[test]
75    fn test_mutable_edges_exact_edge_count_includes_delta() {
76        let mut edges = CsrMutableEdges::with_coords(vec![grid(0, 0), grid(0, 1), grid(0, 2)]);
77        edges.add_edge(VertexId(0), VertexId(1));
78        edges.add_edge(VertexId(0), VertexId(2));
79        assert_eq!(edges.num_edges_exact(), 2);
80
81        edges.remove_edge(VertexId(0), VertexId(1));
82        assert_eq!(edges.num_edges_exact(), 1);
83    }
84
85    #[test]
86    fn test_delta_slab_empty_base() {
87        let csr = CsrEdges::empty();
88        let mut delta = DeltaEdgeSlab::new();
89
90        delta.add_edge(VertexId(0), VertexId(1));
91        delta.add_edge(VertexId(0), VertexId(2));
92
93        let merged = delta.merged_view(&csr, VertexId(0));
94        assert_eq!(merged, vec![VertexId(1), VertexId(2)]);
95    }
96
97    #[test]
98    fn test_delta_slab_remove_nonexistent() {
99        let csr = CsrEdges::from_adjacency(vec![(0u32, vec![1u32])], &[grid(0, 0), grid(0, 1)]);
100        let mut delta = DeltaEdgeSlab::new();
101
102        // Remove edge that doesn't exist
103        delta.remove_edge(VertexId(0), VertexId(2));
104
105        let merged = delta.merged_view(&csr, VertexId(0));
106        assert_eq!(merged, vec![VertexId(1)]); // No change
107    }
108
109    #[test]
110    fn test_delta_slab_apply_to_csr() {
111        let csr = CsrEdges::from_adjacency(
112            vec![(0u32, vec![1u32]), (1u32, vec![2u32]), (2u32, vec![])],
113            &[grid(0, 0), grid(0, 1), grid(1, 0)],
114        );
115
116        let mut delta = DeltaEdgeSlab::new();
117        delta.add_edge(VertexId(0), VertexId(2));
118        delta.remove_edge(VertexId(1), VertexId(2));
119        delta.add_edge(VertexId(2), VertexId(0));
120
121        // Apply delta and get new CSR
122        let coords = vec![grid(0, 0), grid(0, 1), grid(1, 0)];
123        let vertex_ids = vec![0u32, 1u32, 2u32];
124        let new_csr = delta.apply_to_csr(&csr, &coords, &vertex_ids);
125
126        assert_eq!(new_csr.out_edges(VertexId(0)), &[VertexId(1), VertexId(2)]);
127        assert_eq!(new_csr.out_edges(VertexId(1)), &[]);
128        assert_eq!(new_csr.out_edges(VertexId(2)), &[VertexId(0)]);
129    }
130
131    #[test]
132    fn test_mutable_edges_auto_rebuild() {
133        let mut edges = CsrMutableEdges::with_coords(vec![grid(0, 0), grid(0, 1), grid(1, 0)]);
134
135        // Add initial edges
136        edges.add_edge(VertexId(0), VertexId(1));
137        edges.add_edge(VertexId(1), VertexId(2));
138
139        // Perform many operations to trigger rebuild
140        for _ in 0..500 {
141            edges.add_edge(VertexId(2), VertexId(0));
142            edges.remove_edge(VertexId(2), VertexId(0));
143        }
144
145        // Check that rebuild happened (delta is small)
146        assert!(edges.delta_size() < 50);
147
148        // Verify edges are still correct
149        assert_eq!(edges.out_edges(VertexId(0)), vec![VertexId(1)]);
150        assert_eq!(edges.out_edges(VertexId(1)), vec![VertexId(2)]);
151    }
152
153    #[test]
154    fn test_mutable_edges_with_offset_vertex_ids() {
155        use crate::engine::vertex_store::FIRST_NORMAL_VERTEX;
156
157        let mut edges = CsrMutableEdges::new();
158
159        // Add vertices with IDs starting at FIRST_NORMAL_VERTEX (1024)
160        let base_id = FIRST_NORMAL_VERTEX;
161        edges.add_vertex(grid(0, 0), base_id);
162        edges.add_vertex(grid(0, 1), base_id + 1);
163        edges.add_vertex(grid(1, 0), base_id + 2);
164
165        // Add edges using offset IDs
166        edges.add_edge(VertexId(base_id), VertexId(base_id + 1));
167        edges.add_edge(VertexId(base_id + 1), VertexId(base_id + 2));
168        edges.add_edge(VertexId(base_id + 2), VertexId(base_id));
169
170        // Verify edges work correctly
171        assert_eq!(
172            edges.out_edges(VertexId(base_id)),
173            vec![VertexId(base_id + 1)]
174        );
175        assert_eq!(
176            edges.out_edges(VertexId(base_id + 1)),
177            vec![VertexId(base_id + 2)]
178        );
179        assert_eq!(
180            edges.out_edges(VertexId(base_id + 2)),
181            vec![VertexId(base_id)]
182        );
183
184        // Force rebuild and verify again
185        edges.rebuild();
186        assert_eq!(
187            edges.out_edges(VertexId(base_id)),
188            vec![VertexId(base_id + 1)]
189        );
190        assert_eq!(
191            edges.out_edges(VertexId(base_id + 1)),
192            vec![VertexId(base_id + 2)]
193        );
194        assert_eq!(
195            edges.out_edges(VertexId(base_id + 2)),
196            vec![VertexId(base_id)]
197        );
198    }
199
200    #[test]
201    fn test_csr_coord_update() {
202        let mut edges = CsrMutableEdges::new();
203
204        edges.add_vertex(grid(1, 1), 1024);
205        edges.add_vertex(grid(2, 2), 1025);
206        edges.add_edge(VertexId(1024), VertexId(1025));
207
208        // Update coordinate
209        edges.update_addr(VertexId(1024), grid(5, 5));
210
211        // Verify sorting remains correct after rebuild
212        edges.rebuild();
213        let out = edges.out_edges(VertexId(1024));
214        assert_eq!(out, vec![VertexId(1025)]);
215    }
216
217    #[test]
218    fn update_coord_uses_vertex_position_index() {
219        let mut edges = CsrMutableEdges::new();
220        let items: Vec<_> = (0..20_000u32).map(|id| (grid(id, id % 17), id)).collect();
221        edges.add_vertices_batch(&items);
222
223        let started = std::time::Instant::now();
224        for id in 15_000..20_000u32 {
225            edges.update_addr(VertexId(id), grid(id + 1, (id + 2) % 100));
226        }
227        let elapsed = started.elapsed();
228
229        if !cfg!(debug_assertions) {
230            assert!(
231                elapsed < std::time::Duration::from_millis(50),
232                "update_coord took {elapsed:?}"
233            );
234        }
235
236        for id in 15_000..20_000u32 {
237            let pos = edges.vertex_pos[&id];
238            assert_eq!(edges.vertex_ids[pos], id);
239            assert_eq!(edges.coords[pos], grid(id + 1, (id + 2) % 100));
240        }
241    }
242
243    #[test]
244    fn test_last_op_wins_add_then_remove() {
245        let csr = CsrEdges::from_adjacency(vec![(0u32, vec![])], &[grid(0, 0)]);
246        let mut delta = DeltaEdgeSlab::new();
247        delta.add_edge(VertexId(0), VertexId(1));
248        delta.remove_edge(VertexId(0), VertexId(1));
249        let merged = delta.merged_view(&csr, VertexId(0));
250        assert_eq!(merged, vec![]);
251    }
252
253    #[test]
254    fn test_last_op_wins_remove_then_add() {
255        let csr = CsrEdges::from_adjacency(vec![(0u32, vec![])], &[grid(0, 0)]);
256        let mut delta = DeltaEdgeSlab::new();
257        delta.remove_edge(VertexId(0), VertexId(1));
258        delta.add_edge(VertexId(0), VertexId(1));
259        let merged = delta.merged_view(&csr, VertexId(0));
260        assert_eq!(merged, vec![VertexId(1)]);
261    }
262
263    #[test]
264    fn test_dedup_additions_and_sorted() {
265        let csr = CsrEdges::from_adjacency(
266            vec![(0u32, vec![2u32])],
267            &[grid(0, 0), grid(0, 1), grid(0, 2)],
268        );
269        let mut delta = DeltaEdgeSlab::new();
270        // Add duplicates and out-of-order ids
271        delta.add_edge(VertexId(0), VertexId(1));
272        delta.add_edge(VertexId(0), VertexId(3));
273        delta.add_edge(VertexId(0), VertexId(1)); // duplicate
274        let merged = delta.merged_view(&csr, VertexId(0));
275        // Should be deduped and sorted by VertexId
276        assert_eq!(merged, vec![VertexId(1), VertexId(2), VertexId(3)]);
277    }
278
279    #[test]
280    fn test_merged_in_view_add_and_remove() {
281        let csr = CsrEdges::from_adjacency(
282            vec![(0u32, vec![2u32]), (1u32, vec![2u32])],
283            &[grid(0, 0), grid(0, 1), grid(0, 2), grid(0, 3)],
284        );
285        let mut delta = DeltaEdgeSlab::new();
286
287        // New incoming edge 3 -> 2, removed incoming edge 0 -> 2.
288        delta.add_edge(VertexId(3), VertexId(2));
289        delta.remove_edge(VertexId(0), VertexId(2));
290
291        let merged = delta.merged_in_view(&csr, VertexId(2));
292        assert_eq!(merged, vec![VertexId(1), VertexId(3)]);
293    }
294
295    #[test]
296    fn test_merged_in_view_last_op_wins() {
297        let csr = CsrEdges::from_adjacency(vec![(0u32, vec![1u32])], &[grid(0, 0), grid(0, 1)]);
298        let mut delta = DeltaEdgeSlab::new();
299
300        delta.remove_edge(VertexId(0), VertexId(1));
301        delta.add_edge(VertexId(0), VertexId(1));
302        assert_eq!(delta.merged_in_view(&csr, VertexId(1)), vec![VertexId(0)]);
303
304        delta.add_edge(VertexId(2), VertexId(1));
305        delta.remove_edge(VertexId(2), VertexId(1));
306        assert_eq!(delta.merged_in_view(&csr, VertexId(1)), vec![VertexId(0)]);
307    }
308
309    #[test]
310    fn test_end_batch_below_threshold_defers_rebuild() {
311        let mut edges = CsrMutableEdges::with_coords(vec![grid(0, 0), grid(0, 1), grid(0, 2)]);
312        let before = edges.rebuild_count();
313
314        edges.begin_batch();
315        edges.add_edge(VertexId(0), VertexId(1));
316        edges.add_edge(VertexId(0), VertexId(2));
317        edges.end_batch();
318
319        // Small edit bursts must not trigger a full rebuild (#125)...
320        assert_eq!(edges.rebuild_count(), before);
321        // ...while reads stay correct through the merged views.
322        assert_eq!(edges.out_edges(VertexId(0)), vec![VertexId(1), VertexId(2)]);
323        assert_eq!(edges.in_edges_merged(VertexId(1)), vec![VertexId(0)]);
324        assert_eq!(edges.in_edges_merged(VertexId(2)), vec![VertexId(0)]);
325    }
326
327    #[test]
328    fn test_end_batch_rebuilds_at_threshold() {
329        let coords: Vec<VertexAddr> = (0..1200u32).map(|i| grid(i, 0)).collect();
330        let mut edges = CsrMutableEdges::with_coords(coords);
331        let before = edges.rebuild_count();
332
333        edges.begin_batch();
334        for i in 0..1100u32 {
335            edges.add_edge(VertexId(i), VertexId(i + 1));
336        }
337        edges.end_batch();
338
339        assert_eq!(edges.rebuild_count(), before + 1);
340        assert_eq!(edges.delta_size(), 0);
341        assert_eq!(edges.out_edges(VertexId(0)), vec![VertexId(1)]);
342        assert_eq!(edges.in_edges(VertexId(1)), &[VertexId(0)]);
343    }
344
345    #[test]
346    fn test_add_vertex_defers_rebuild() {
347        let mut edges = CsrMutableEdges::new();
348        edges.add_vertex(grid(0, 0), 1024);
349        edges.add_vertex(grid(0, 1), 1025);
350        assert_eq!(edges.rebuild_count(), 0);
351
352        edges.add_edge(VertexId(1024), VertexId(1025));
353        // Reads see the new vertex/edge before any rebuild.
354        assert_eq!(edges.out_edges(VertexId(1024)), vec![VertexId(1025)]);
355        assert_eq!(edges.in_edges_merged(VertexId(1025)), vec![VertexId(1024)]);
356
357        // And the next rebuild folds the vertex into the CSR base.
358        edges.rebuild();
359        assert_eq!(edges.out_edges(VertexId(1024)), vec![VertexId(1025)]);
360        assert_eq!(edges.in_edges(VertexId(1025)), &[VertexId(1024)]);
361    }
362
363    #[test]
364    fn test_end_batch_rebuilds_on_coord_change_only() {
365        let mut edges = CsrMutableEdges::with_coords(vec![grid(0, 0), grid(0, 1)]);
366        edges.begin_batch();
367        edges.update_addr(VertexId(0), grid(0, 2));
368        // No ops, only coord changed; end_batch should rebuild due to coord_dirty
369        edges.end_batch();
370        // Smoke: out_edges call should not panic and reflect empty edges
371        assert_eq!(edges.out_edges(VertexId(0)), Vec::<VertexId>::new());
372    }
373}
374
375/// Delta slab for accumulating edge mutations between CSR rebuilds
376///
377/// Provides O(1) edge mutations by tracking additions and removals
378/// separately, merging them with the base CSR on read.
379#[derive(Debug)]
380pub struct DeltaEdgeSlab {
381    /// New edges to add, grouped by source vertex (set semantics to avoid duplicates)
382    additions: FxHashMap<VertexId, FxHashSet<VertexId>>,
383
384    /// Edges to remove, stored as sets for O(1) lookup
385    removals: FxHashMap<VertexId, FxHashSet<VertexId>>,
386
387    /// Reverse index of `additions`: target -> sources. Keeps incoming-edge
388    /// reads delta-aware in O(degree) instead of O(V) scans (#125).
389    additions_in: FxHashMap<VertexId, FxHashSet<VertexId>>,
390
391    /// Reverse index of `removals`: target -> sources.
392    removals_in: FxHashMap<VertexId, FxHashSet<VertexId>>,
393
394    /// Total operation count for rebuild threshold
395    op_count: usize,
396
397    /// Flag indicating coordinates have changed and rebuild is needed
398    coord_changed: bool,
399}
400
401impl DeltaEdgeSlab {
402    /// Create a new empty delta slab
403    pub fn new() -> Self {
404        Self {
405            additions: FxHashMap::default(),
406            removals: FxHashMap::default(),
407            additions_in: FxHashMap::default(),
408            removals_in: FxHashMap::default(),
409            op_count: 0,
410            coord_changed: false,
411        }
412    }
413
414    fn reserve_additions(&mut self, additional: usize) {
415        self.additions.reserve(additional);
416        self.additions_in.reserve(additional);
417    }
418
419    /// Add an edge from source to target
420    pub fn add_edge(&mut self, from: VertexId, to: VertexId) {
421        // Last-op-wins: if previously removed, cancel the removal
422        if let Some(rem) = self.removals.get_mut(&from) {
423            rem.remove(&to);
424        }
425        if let Some(rem) = self.removals_in.get_mut(&to) {
426            rem.remove(&from);
427        }
428        // Insert into additions set (forward and reverse)
429        self.additions.entry(from).or_default().insert(to);
430        self.additions_in.entry(to).or_default().insert(from);
431        self.op_count += 1;
432    }
433
434    /// Remove an edge from source to target
435    pub fn remove_edge(&mut self, from: VertexId, to: VertexId) {
436        // Last-op-wins: if previously added in this slab, cancel the addition
437        if let Some(adds) = self.additions.get_mut(&from) {
438            adds.remove(&to);
439        }
440        if let Some(adds) = self.additions_in.get_mut(&to) {
441            adds.remove(&from);
442        }
443        // Record removal (forward and reverse)
444        self.removals.entry(from).or_default().insert(to);
445        self.removals_in.entry(to).or_default().insert(from);
446        self.op_count += 1;
447    }
448
449    /// Get a merged view of edges for a vertex, combining CSR and delta
450    pub fn merged_view(&self, csr: &CsrEdges, v: VertexId) -> Vec<VertexId> {
451        // Start from base CSR out-edges
452        let mut result: Vec<_> = csr.out_edges(v).to_vec();
453        // Remove edges marked for deletion
454        if let Some(removes) = self.removals.get(&v) {
455            result.retain(|e| !removes.contains(e));
456        }
457        // Add new edges (set semantics)
458        if let Some(adds) = self.additions.get(&v) {
459            result.extend(adds.iter().copied());
460        }
461        // Dedup deterministically and sort by VertexId for stable order
462        let mut seen: FxHashSet<VertexId> = FxHashSet::default();
463        result.retain(|e| seen.insert(*e));
464        result.sort_by_key(|e| e.0);
465        result
466    }
467
468    /// Get a merged view of *incoming* edges for a vertex, combining the CSR
469    /// reverse edges with the delta's reverse index. O(in-degree), not O(V).
470    pub fn merged_in_view(&self, csr: &CsrEdges, v: VertexId) -> Vec<VertexId> {
471        let mut result: Vec<_> = csr.in_edges(v).to_vec();
472        if let Some(removes) = self.removals_in.get(&v) {
473            result.retain(|e| !removes.contains(e));
474        }
475        if let Some(adds) = self.additions_in.get(&v) {
476            result.extend(adds.iter().copied());
477        }
478        let mut seen: FxHashSet<VertexId> = FxHashSet::default();
479        result.retain(|e| seen.insert(*e));
480        result.sort_by_key(|e| e.0);
481        result
482    }
483
484    /// Check if the delta needs to be applied (rebuild threshold reached)
485    pub fn needs_rebuild(&self) -> bool {
486        self.op_count >= 1000 || self.coord_changed
487    }
488
489    /// Mark that coordinates have changed and rebuild is needed
490    pub fn mark_dirty(&mut self) {
491        self.coord_changed = true;
492    }
493
494    /// Get the current operation count
495    pub fn op_count(&self) -> usize {
496        self.op_count
497    }
498
499    /// Clear the delta slab
500    pub fn clear(&mut self) {
501        self.additions.clear();
502        self.removals.clear();
503        self.additions_in.clear();
504        self.removals_in.clear();
505        self.op_count = 0;
506        self.coord_changed = false;
507    }
508
509    /// Iterate over all (from, &additions) pairs in the delta. Used by
510    /// build_from_adjacency to carry forward delta-only edges that the
511    /// adjacency input does not cover.
512    pub fn additions_iter(&self) -> impl Iterator<Item = (&VertexId, &FxHashSet<VertexId>)> {
513        self.additions.iter()
514    }
515
516    /// Return removals scheduled for `from` (empty slice if none).
517    pub fn removals_for(&self, from: VertexId) -> impl Iterator<Item = VertexId> + '_ {
518        self.removals
519            .get(&from)
520            .into_iter()
521            .flat_map(|set| set.iter().copied())
522    }
523
524    /// Apply delta to CSR, creating a new CSR structure
525    pub fn apply_to_csr(
526        &self,
527        base: &CsrEdges,
528        coords: &[VertexAddr],
529        vertex_ids: &[u32],
530    ) -> CsrEdges {
531        let mut adjacency = Vec::with_capacity(vertex_ids.len());
532
533        // Build new adjacency list by merging base and delta
534        for &vid in vertex_ids {
535            let v = VertexId(vid);
536            let merged = self.merged_view(base, v);
537
538            // Convert to u32 for adjacency format
539            let targets: Vec<u32> = merged.into_iter().map(|id| id.0).collect();
540
541            adjacency.push((vid, targets));
542        }
543
544        CsrEdges::from_adjacency(adjacency, coords)
545    }
546}
547
548impl Default for DeltaEdgeSlab {
549    fn default() -> Self {
550        Self::new()
551    }
552}
553
554/// Mutable edge storage combining CSR base with delta slab
555///
556/// Provides efficient edge mutations with automatic rebuild when
557/// delta grows too large.
558#[derive(Debug)]
559pub struct CsrMutableEdges {
560    /// Base CSR structure (immutable between rebuilds)
561    base: CsrEdges,
562
563    /// Delta slab for mutations
564    delta: DeltaEdgeSlab,
565
566    /// Vertex addresses (grid position or symbol identity) for deterministic ordering
567    coords: Vec<VertexAddr>,
568
569    /// Vertex IDs corresponding to coords array
570    vertex_ids: Vec<u32>,
571
572    /// Position of each vertex id in the coords and vertex_ids arrays.
573    vertex_pos: FxHashMap<u32, usize>,
574
575    /// Nested batch depth; non-zero defers automatic rebuilds.
576    batch_depth: usize,
577
578    /// Number of full CSR rebuilds performed (observability / regression tests).
579    rebuild_count: u64,
580}
581
582impl CsrMutableEdges {
583    /// Create new mutable edges with empty base
584    pub fn new() -> Self {
585        Self {
586            base: CsrEdges::empty(),
587            delta: DeltaEdgeSlab::new(),
588            coords: Vec::new(),
589            vertex_ids: Vec::new(),
590            vertex_pos: FxHashMap::default(),
591            batch_depth: 0,
592            rebuild_count: 0,
593        }
594    }
595
596    /// Create with initial vertex coordinates
597    pub fn with_coords(coords: Vec<VertexAddr>) -> Self {
598        let num_vertices = coords.len();
599        let vertex_ids: Vec<u32> = (0..num_vertices as u32).collect();
600        let adjacency: Vec<_> = vertex_ids.iter().map(|&id| (id, Vec::new())).collect();
601        let vertex_pos = vertex_ids
602            .iter()
603            .enumerate()
604            .map(|(idx, &id)| (id, idx))
605            .collect();
606
607        Self {
608            base: CsrEdges::from_adjacency(adjacency, &coords),
609            delta: DeltaEdgeSlab::new(),
610            coords,
611            vertex_ids,
612            vertex_pos,
613            batch_depth: 0,
614            rebuild_count: 0,
615        }
616    }
617
618    pub(crate) fn reserve_prepared_additions(&mut self, vertices: usize, edges: usize) {
619        self.coords.reserve(vertices);
620        self.vertex_ids.reserve(vertices);
621        self.vertex_pos.reserve(vertices);
622        self.delta.reserve_additions(edges);
623    }
624
625    /// Add an edge, rebuilding if threshold reached
626    pub fn add_edge(&mut self, from: VertexId, to: VertexId) {
627        self.delta.add_edge(from, to);
628        self.maybe_rebuild();
629    }
630
631    /// Remove an edge, rebuilding if threshold reached
632    pub fn remove_edge(&mut self, from: VertexId, to: VertexId) {
633        self.delta.remove_edge(from, to);
634        self.maybe_rebuild();
635    }
636
637    /// Get outgoing edges for a vertex (merged view)
638    pub fn out_edges(&self, v: VertexId) -> Vec<VertexId> {
639        if self.delta.op_count() == 0 {
640            self.base.out_edges(v).to_vec()
641        } else {
642            self.delta.merged_view(&self.base, v)
643        }
644    }
645
646    /// Borrow outgoing edges when no delta mutations are pending.
647    ///
648    /// This is a zero-allocation hot path for read-heavy evaluation/scheduling phases.
649    #[inline]
650    pub fn out_edges_ref(&self, v: VertexId) -> Option<&[VertexId]> {
651        if self.delta.op_count() == 0 {
652            Some(self.base.out_edges(v))
653        } else {
654            None
655        }
656    }
657
658    /// Get incoming edges from base CSR (delta not applied for performance)
659    /// After rebuild, this will include all changes
660    pub fn in_edges(&self, v: VertexId) -> &[VertexId] {
661        self.base.in_edges(v)
662    }
663
664    /// Get incoming edges for a vertex with pending delta mutations applied.
665    ///
666    /// O(in-degree + pending delta entries for `v`); never scans all vertices.
667    pub fn in_edges_merged(&self, v: VertexId) -> Vec<VertexId> {
668        if self.delta.op_count() == 0 {
669            self.base.in_edges(v).to_vec()
670        } else {
671            self.delta.merged_in_view(&self.base, v)
672        }
673    }
674
675    /// Borrow incoming edges when no delta mutations are pending.
676    ///
677    /// This is a zero-allocation hot path for read-heavy evaluation/scheduling phases.
678    #[inline]
679    pub fn in_edges_ref(&self, v: VertexId) -> Option<&[VertexId]> {
680        if self.delta.op_count() == 0 {
681            Some(self.base.in_edges(v))
682        } else {
683            None
684        }
685    }
686
687    /// Visit incoming edges without materializing the complete in-degree.
688    ///
689    /// The caller-owned budget is charged once for every base or delta entry
690    /// examined. `false` means an entry remained when the budget was
691    /// exhausted. Pending removals are filtered and pending additions are read
692    /// through the reverse delta index, so this has the same delta-aware
693    /// semantics as [`Self::in_edges_merged`]. A duplicate base/addition pair
694    /// may be presented twice; bounded semantic callers already deduplicate by
695    /// address and charging the duplicate is the conservative accounting rule.
696    pub(crate) fn visit_in_edges_bounded(
697        &self,
698        v: VertexId,
699        remaining_work: &mut u64,
700        visitor: &mut dyn FnMut(VertexId) -> bool,
701    ) -> bool {
702        let removals = self.delta.removals_in.get(&v);
703        for &source in self.base.in_edges(v) {
704            if *remaining_work == 0 {
705                return false;
706            }
707            *remaining_work -= 1;
708            if removals.is_some_and(|set| set.contains(&source)) {
709                continue;
710            }
711            if !visitor(source) {
712                return false;
713            }
714        }
715        if let Some(additions) = self.delta.additions_in.get(&v) {
716            for &source in additions {
717                if *remaining_work == 0 {
718                    return false;
719                }
720                *remaining_work -= 1;
721                if !visitor(source) {
722                    return false;
723                }
724            }
725        }
726        true
727    }
728
729    /// Get the current delta size
730    pub fn delta_size(&self) -> usize {
731        self.delta.op_count()
732    }
733
734    /// Return the exact number of logical outgoing dependency edges, including pending delta
735    /// mutations.
736    ///
737    /// This is intended for read-only observability. When the delta slab is non-empty, the
738    /// implementation walks the known vertex ids and merges each outgoing edge list, so callers
739    /// should avoid putting it on hot evaluation paths.
740    pub fn num_edges_exact(&self) -> usize {
741        if self.delta.op_count() == 0 {
742            return self.base.num_edges();
743        }
744
745        self.vertex_ids
746            .iter()
747            .map(|&id| self.out_edges(VertexId(id)).len())
748            .sum()
749    }
750
751    /// Force a rebuild of the CSR structure
752    pub fn rebuild(&mut self) {
753        if self.delta.op_count() > 0 || self.delta.needs_rebuild() {
754            self.base = self
755                .delta
756                .apply_to_csr(&self.base, &self.coords, &self.vertex_ids);
757            self.delta.clear();
758            self.rebuild_count += 1;
759        }
760    }
761
762    /// Number of full CSR rebuilds performed so far.
763    ///
764    /// Per-edit dependency updates must amortize rebuilds (#125); regression
765    /// tests assert on this counter instead of wall-clock time.
766    pub fn rebuild_count(&self) -> u64 {
767        self.rebuild_count
768    }
769
770    /// Check and perform rebuild if threshold reached
771    fn maybe_rebuild(&mut self) {
772        if self.batch_depth == 0 && self.delta.needs_rebuild() {
773            self.rebuild();
774        }
775    }
776
777    /// Enter batch mode - defer rebuilds until the outer end_batch() call.
778    pub fn begin_batch(&mut self) {
779        self.batch_depth = self.batch_depth.saturating_add(1);
780    }
781
782    /// Exit batch mode and rebuild only when the amortization threshold (or a
783    /// coordinate change) demands it.
784    ///
785    /// Rebuilding unconditionally here made every single-formula edit O(V)
786    /// and cell-by-cell edit loops O(N^2) (#125). Pending delta mutations are
787    /// visible to readers through the merged out/in views, so deferring the
788    /// rebuild is safe.
789    pub fn end_batch(&mut self) {
790        self.batch_depth = self.batch_depth.saturating_sub(1);
791        if self.batch_depth == 0 && self.delta.needs_rebuild() {
792            self.rebuild();
793        }
794    }
795
796    /// End a batch without compacting the delta slab into the full CSR.
797    /// Prepared graph transactions use this to keep application work local.
798    pub(crate) fn end_batch_deferred(&mut self) {
799        self.batch_depth = self.batch_depth.saturating_sub(1);
800    }
801
802    /// Add a new vertex with its coordinate and ID
803    ///
804    /// Does NOT rebuild the CSR base: reads of a vertex that is not in the
805    /// base yet gracefully resolve to "no edges" plus any pending delta
806    /// mutations, and the next rebuild picks the vertex up from
807    /// `coords`/`vertex_ids` (#125).
808    pub fn add_vertex(&mut self, addr: VertexAddr, vertex_id: u32) -> usize {
809        let idx = self.coords.len();
810        self.coords.push(addr);
811        self.vertex_ids.push(vertex_id);
812        self.vertex_pos.insert(vertex_id, idx);
813        idx
814    }
815
816    /// Add many vertices at once; single rebuild at end.
817    pub fn add_vertices_batch(&mut self, items: &[(VertexAddr, u32)]) {
818        if items.is_empty() {
819            return;
820        }
821        let start_len = self.coords.len();
822        self.coords.reserve(items.len());
823        self.vertex_ids.reserve(items.len());
824        for (addr, vid) in items {
825            let idx = self.coords.len();
826            self.coords.push(*addr);
827            self.vertex_ids.push(*vid);
828            self.vertex_pos.insert(*vid, idx);
829        }
830        // Single rebuild to incorporate all new vertices.
831        self.rebuild();
832        debug_assert_eq!(self.coords.len(), start_len + items.len());
833    }
834
835    /// Update the stored address for a vertex in the cache
836    /// Marks for rebuild to maintain sort order
837    pub fn update_addr(&mut self, vertex_id: VertexId, new_addr: VertexAddr) {
838        if let Some(&pos) = self.vertex_pos.get(&vertex_id.0) {
839            debug_assert_eq!(
840                self.vertex_ids[pos], vertex_id.0,
841                "vertex_pos out of sync with vertex_ids at position {pos}"
842            );
843            self.coords[pos] = new_addr;
844            // Force rebuild on next access to maintain sort invariants
845            self.delta.mark_dirty();
846        }
847    }
848
849    /// Return a copy of `adjacency` extended with the current base+delta
850    /// out-edges of every existing vertex that the input does not cover.
851    ///
852    /// Named-range pass-through vertices (NamedScalar/NamedArray) emit edges
853    /// to their underlying cells via `add_edge` during load; those edges live
854    /// in `base`/`delta` but are not part of the formula-target adjacency that
855    /// bulk-ingest's finalize hands to [`build_from_adjacency`]. Feeding the
856    /// raw adjacency straight to that (pure) builder would therefore silently
857    /// drop the pass-through vertices' out-edges, and `build_demand_subgraph`
858    /// could never reach the underlying cells. Callers run this first to merge
859    /// those edges back in, then pass the result to `build_from_adjacency`.
860    ///
861    /// Must be called BEFORE `build_from_adjacency`, which overwrites
862    /// `base`/`delta`/`vertex_ids`.
863    pub fn adjacency_with_carried_forward_edges(
864        &self,
865        mut adjacency: Vec<(u32, Vec<u32>)>,
866    ) -> Vec<(u32, Vec<u32>)> {
867        let covered: FxHashSet<u32> = adjacency.iter().map(|(vid, _)| *vid).collect();
868        // Carry forward base+delta out-edges for ANY existing vertex not in
869        // the new adjacency input. `vertex_ids` already tracks every vertex
870        // allocated so far, including named-range pass-through vertices.
871        for &vid in &self.vertex_ids {
872            if covered.contains(&vid) {
873                continue;
874            }
875            let v = VertexId(vid);
876            // Merge with any pending delta edges for this vertex.
877            let merged = if self.delta.op_count() == 0 {
878                self.base.out_edges(v).to_vec()
879            } else {
880                self.delta.merged_view(&self.base, v)
881            };
882            if !merged.is_empty() {
883                adjacency.push((vid, merged.into_iter().map(|v| v.0).collect()));
884            }
885        }
886        // Also carry forward delta-only additions (additions to vertices that
887        // weren't in vertex_ids yet — e.g., freshly allocated names whose
888        // add_vertex didn't trigger a rebuild). Pure deltas should be rare
889        // here, but include them for completeness.
890        for (&from, adds) in self.delta.additions_iter() {
891            if covered.contains(&from.0) {
892                continue;
893            }
894            if adjacency.iter().any(|(v, _)| *v == from.0) {
895                continue;
896            }
897            let removals: FxHashSet<u32> = self.delta.removals_for(from).map(|v| v.0).collect();
898            let mut base_set: FxHashSet<u32> =
899                self.base.out_edges(from).iter().map(|v| v.0).collect();
900            for r in &removals {
901                base_set.remove(r);
902            }
903            for &add in adds {
904                base_set.insert(add.0);
905            }
906            if !base_set.is_empty() {
907                let mut targets: Vec<u32> = base_set.into_iter().collect();
908                targets.sort_unstable();
909                adjacency.push((from.0, targets));
910            }
911        }
912        adjacency
913    }
914
915    /// Build underlying CSR directly from adjacency and provided coords/ids.
916    /// This replaces the current base and clears the delta slab.
917    ///
918    /// Pure builder: it uses exactly the edges in `adjacency` and does not
919    /// consult the existing `base`/`delta`. To preserve edges for vertices
920    /// absent from `adjacency` (e.g. named-range pass-through vertices), run
921    /// [`adjacency_with_carried_forward_edges`] first and pass its result in.
922    pub fn build_from_adjacency(
923        &mut self,
924        adjacency: Vec<(u32, Vec<u32>)>,
925        coords: Vec<VertexAddr>,
926        vertex_ids: Vec<u32>,
927    ) {
928        self.base = CsrEdges::from_adjacency(adjacency, &coords);
929        self.coords = coords;
930        self.vertex_ids = vertex_ids;
931        self.vertex_pos = self
932            .vertex_ids
933            .iter()
934            .enumerate()
935            .map(|(idx, &id)| (id, idx))
936            .collect();
937        self.delta.clear();
938    }
939}
940
941impl Default for CsrMutableEdges {
942    fn default() -> Self {
943        Self::new()
944    }
945}