Skip to main content

formualizer_eval/engine/
delta_edges.rs

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