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    /// Add an edge from source to target
462    pub fn add_edge(&mut self, from: VertexId, to: VertexId) {
463        // Last-op-wins: if previously removed, cancel the removal
464        if let Some(rem) = self.removals.get_mut(&from) {
465            rem.remove(&to);
466        }
467        if let Some(rem) = self.removals_in.get_mut(&to) {
468            rem.remove(&from);
469        }
470        // Insert into additions set (forward and reverse)
471        self.additions.entry(from).or_default().insert(to);
472        self.additions_in.entry(to).or_default().insert(from);
473        self.op_count += 1;
474    }
475
476    /// Remove an edge from source to target
477    pub fn remove_edge(&mut self, from: VertexId, to: VertexId) {
478        // Last-op-wins: if previously added in this slab, cancel the addition
479        if let Some(adds) = self.additions.get_mut(&from) {
480            adds.remove(&to);
481        }
482        if let Some(adds) = self.additions_in.get_mut(&to) {
483            adds.remove(&from);
484        }
485        // Record removal (forward and reverse)
486        self.removals.entry(from).or_default().insert(to);
487        self.removals_in.entry(to).or_default().insert(from);
488        self.op_count += 1;
489    }
490
491    /// Get a merged view of edges for a vertex, combining CSR and delta
492    pub fn merged_view(&self, csr: &CsrEdges, v: VertexId) -> Vec<VertexId> {
493        // Start from base CSR out-edges
494        let mut result: Vec<_> = csr.out_edges(v).to_vec();
495        // Remove edges marked for deletion
496        if let Some(removes) = self.removals.get(&v) {
497            result.retain(|e| !removes.contains(e));
498        }
499        // Add new edges (set semantics)
500        if let Some(adds) = self.additions.get(&v) {
501            result.extend(adds.iter().copied());
502        }
503        // Dedup deterministically and sort by VertexId for stable order
504        let mut seen: FxHashSet<VertexId> = FxHashSet::default();
505        result.retain(|e| seen.insert(*e));
506        result.sort_by_key(|e| e.0);
507        result
508    }
509
510    /// Get a merged view of *incoming* edges for a vertex, combining the CSR
511    /// reverse edges with the delta's reverse index. O(in-degree), not O(V).
512    pub fn merged_in_view(&self, csr: &CsrEdges, v: VertexId) -> Vec<VertexId> {
513        let mut result: Vec<_> = csr.in_edges(v).to_vec();
514        if let Some(removes) = self.removals_in.get(&v) {
515            result.retain(|e| !removes.contains(e));
516        }
517        if let Some(adds) = self.additions_in.get(&v) {
518            result.extend(adds.iter().copied());
519        }
520        let mut seen: FxHashSet<VertexId> = FxHashSet::default();
521        result.retain(|e| seen.insert(*e));
522        result.sort_by_key(|e| e.0);
523        result
524    }
525
526    /// Check if the delta needs to be applied (rebuild threshold reached)
527    pub fn needs_rebuild(&self) -> bool {
528        self.op_count >= 1000 || self.coord_changed
529    }
530
531    /// Mark that coordinates have changed and rebuild is needed
532    pub fn mark_dirty(&mut self) {
533        self.coord_changed = true;
534    }
535
536    /// Get the current operation count
537    pub fn op_count(&self) -> usize {
538        self.op_count
539    }
540
541    /// Clear the delta slab
542    pub fn clear(&mut self) {
543        self.additions.clear();
544        self.removals.clear();
545        self.additions_in.clear();
546        self.removals_in.clear();
547        self.op_count = 0;
548        self.coord_changed = false;
549    }
550
551    /// Iterate over all (from, &additions) pairs in the delta. Used by
552    /// build_from_adjacency to carry forward delta-only edges that the
553    /// adjacency input does not cover.
554    pub fn additions_iter(&self) -> impl Iterator<Item = (&VertexId, &FxHashSet<VertexId>)> {
555        self.additions.iter()
556    }
557
558    /// Return removals scheduled for `from` (empty slice if none).
559    pub fn removals_for(&self, from: VertexId) -> impl Iterator<Item = VertexId> + '_ {
560        self.removals
561            .get(&from)
562            .into_iter()
563            .flat_map(|set| set.iter().copied())
564    }
565
566    /// Apply delta to CSR, creating a new CSR structure
567    pub fn apply_to_csr(
568        &self,
569        base: &CsrEdges,
570        coords: &[AbsCoord],
571        vertex_ids: &[u32],
572    ) -> CsrEdges {
573        let mut adjacency = Vec::with_capacity(vertex_ids.len());
574
575        // Build new adjacency list by merging base and delta
576        for &vid in vertex_ids {
577            let v = VertexId(vid);
578            let merged = self.merged_view(base, v);
579
580            // Convert to u32 for adjacency format
581            let targets: Vec<u32> = merged.into_iter().map(|id| id.0).collect();
582
583            adjacency.push((vid, targets));
584        }
585
586        CsrEdges::from_adjacency(adjacency, coords)
587    }
588}
589
590impl Default for DeltaEdgeSlab {
591    fn default() -> Self {
592        Self::new()
593    }
594}
595
596/// Mutable edge storage combining CSR base with delta slab
597///
598/// Provides efficient edge mutations with automatic rebuild when
599/// delta grows too large.
600#[derive(Debug)]
601pub struct CsrMutableEdges {
602    /// Base CSR structure (immutable between rebuilds)
603    base: CsrEdges,
604
605    /// Delta slab for mutations
606    delta: DeltaEdgeSlab,
607
608    /// Vertex coordinates for deterministic ordering
609    coords: Vec<AbsCoord>,
610
611    /// Vertex IDs corresponding to coords array
612    vertex_ids: Vec<u32>,
613
614    /// Position of each vertex id in the coords and vertex_ids arrays.
615    vertex_pos: FxHashMap<u32, usize>,
616
617    /// Nested batch depth; non-zero defers automatic rebuilds.
618    batch_depth: usize,
619
620    /// Number of full CSR rebuilds performed (observability / regression tests).
621    rebuild_count: u64,
622}
623
624impl CsrMutableEdges {
625    /// Create new mutable edges with empty base
626    pub fn new() -> Self {
627        Self {
628            base: CsrEdges::empty(),
629            delta: DeltaEdgeSlab::new(),
630            coords: Vec::new(),
631            vertex_ids: Vec::new(),
632            vertex_pos: FxHashMap::default(),
633            batch_depth: 0,
634            rebuild_count: 0,
635        }
636    }
637
638    /// Create with initial vertex coordinates
639    pub fn with_coords(coords: Vec<AbsCoord>) -> Self {
640        let num_vertices = coords.len();
641        let vertex_ids: Vec<u32> = (0..num_vertices as u32).collect();
642        let adjacency: Vec<_> = vertex_ids.iter().map(|&id| (id, Vec::new())).collect();
643        let vertex_pos = vertex_ids
644            .iter()
645            .enumerate()
646            .map(|(idx, &id)| (id, idx))
647            .collect();
648
649        Self {
650            base: CsrEdges::from_adjacency(adjacency, &coords),
651            delta: DeltaEdgeSlab::new(),
652            coords,
653            vertex_ids,
654            vertex_pos,
655            batch_depth: 0,
656            rebuild_count: 0,
657        }
658    }
659
660    /// Add an edge, rebuilding if threshold reached
661    pub fn add_edge(&mut self, from: VertexId, to: VertexId) {
662        self.delta.add_edge(from, to);
663        self.maybe_rebuild();
664    }
665
666    /// Remove an edge, rebuilding if threshold reached
667    pub fn remove_edge(&mut self, from: VertexId, to: VertexId) {
668        self.delta.remove_edge(from, to);
669        self.maybe_rebuild();
670    }
671
672    /// Get outgoing edges for a vertex (merged view)
673    pub fn out_edges(&self, v: VertexId) -> Vec<VertexId> {
674        if self.delta.op_count() == 0 {
675            self.base.out_edges(v).to_vec()
676        } else {
677            self.delta.merged_view(&self.base, v)
678        }
679    }
680
681    /// Borrow outgoing edges when no delta mutations are pending.
682    ///
683    /// This is a zero-allocation hot path for read-heavy evaluation/scheduling phases.
684    #[inline]
685    pub fn out_edges_ref(&self, v: VertexId) -> Option<&[VertexId]> {
686        if self.delta.op_count() == 0 {
687            Some(self.base.out_edges(v))
688        } else {
689            None
690        }
691    }
692
693    /// Get incoming edges from base CSR (delta not applied for performance)
694    /// After rebuild, this will include all changes
695    pub fn in_edges(&self, v: VertexId) -> &[VertexId] {
696        self.base.in_edges(v)
697    }
698
699    /// Get incoming edges for a vertex with pending delta mutations applied.
700    ///
701    /// O(in-degree + pending delta entries for `v`); never scans all vertices.
702    pub fn in_edges_merged(&self, v: VertexId) -> Vec<VertexId> {
703        if self.delta.op_count() == 0 {
704            self.base.in_edges(v).to_vec()
705        } else {
706            self.delta.merged_in_view(&self.base, v)
707        }
708    }
709
710    /// Borrow incoming edges when no delta mutations are pending.
711    ///
712    /// This is a zero-allocation hot path for read-heavy evaluation/scheduling phases.
713    #[inline]
714    pub fn in_edges_ref(&self, v: VertexId) -> Option<&[VertexId]> {
715        if self.delta.op_count() == 0 {
716            Some(self.base.in_edges(v))
717        } else {
718            None
719        }
720    }
721
722    /// Get the current delta size
723    pub fn delta_size(&self) -> usize {
724        self.delta.op_count()
725    }
726
727    /// Return the exact number of logical outgoing dependency edges, including pending delta
728    /// mutations.
729    ///
730    /// This is intended for read-only observability. When the delta slab is non-empty, the
731    /// implementation walks the known vertex ids and merges each outgoing edge list, so callers
732    /// should avoid putting it on hot evaluation paths.
733    pub fn num_edges_exact(&self) -> usize {
734        if self.delta.op_count() == 0 {
735            return self.base.num_edges();
736        }
737
738        self.vertex_ids
739            .iter()
740            .map(|&id| self.out_edges(VertexId(id)).len())
741            .sum()
742    }
743
744    /// Force a rebuild of the CSR structure
745    pub fn rebuild(&mut self) {
746        if self.delta.op_count() > 0 || self.delta.needs_rebuild() {
747            self.base = self
748                .delta
749                .apply_to_csr(&self.base, &self.coords, &self.vertex_ids);
750            self.delta.clear();
751            self.rebuild_count += 1;
752        }
753    }
754
755    /// Number of full CSR rebuilds performed so far.
756    ///
757    /// Per-edit dependency updates must amortize rebuilds (#125); regression
758    /// tests assert on this counter instead of wall-clock time.
759    pub fn rebuild_count(&self) -> u64 {
760        self.rebuild_count
761    }
762
763    /// Check and perform rebuild if threshold reached
764    fn maybe_rebuild(&mut self) {
765        if self.batch_depth == 0 && self.delta.needs_rebuild() {
766            self.rebuild();
767        }
768    }
769
770    /// Enter batch mode - defer rebuilds until the outer end_batch() call.
771    pub fn begin_batch(&mut self) {
772        self.batch_depth = self.batch_depth.saturating_add(1);
773    }
774
775    /// Exit batch mode and rebuild only when the amortization threshold (or a
776    /// coordinate change) demands it.
777    ///
778    /// Rebuilding unconditionally here made every single-formula edit O(V)
779    /// and cell-by-cell edit loops O(N^2) (#125). Pending delta mutations are
780    /// visible to readers through the merged out/in views, so deferring the
781    /// rebuild is safe.
782    pub fn end_batch(&mut self) {
783        self.batch_depth = self.batch_depth.saturating_sub(1);
784        if self.batch_depth == 0 && self.delta.needs_rebuild() {
785            self.rebuild();
786        }
787    }
788
789    /// Add a new vertex with its coordinate and ID
790    ///
791    /// Does NOT rebuild the CSR base: reads of a vertex that is not in the
792    /// base yet gracefully resolve to "no edges" plus any pending delta
793    /// mutations, and the next rebuild picks the vertex up from
794    /// `coords`/`vertex_ids` (#125).
795    pub fn add_vertex(&mut self, coord: AbsCoord, vertex_id: u32) -> usize {
796        let idx = self.coords.len();
797        self.coords.push(coord);
798        self.vertex_ids.push(vertex_id);
799        self.vertex_pos.insert(vertex_id, idx);
800        idx
801    }
802
803    /// Add many vertices at once; single rebuild at end.
804    pub fn add_vertices_batch(&mut self, items: &[(AbsCoord, u32)]) {
805        if items.is_empty() {
806            return;
807        }
808        let start_len = self.coords.len();
809        self.coords.reserve(items.len());
810        self.vertex_ids.reserve(items.len());
811        for (coord, vid) in items {
812            let idx = self.coords.len();
813            self.coords.push(*coord);
814            self.vertex_ids.push(*vid);
815            self.vertex_pos.insert(*vid, idx);
816        }
817        // Single rebuild to incorporate all new vertices.
818        self.rebuild();
819        debug_assert_eq!(self.coords.len(), start_len + items.len());
820    }
821
822    /// Update coordinate for a vertex in the cache
823    /// Marks for rebuild to maintain sort order
824    pub fn update_coord(&mut self, vertex_id: VertexId, new_coord: AbsCoord) {
825        if let Some(&pos) = self.vertex_pos.get(&vertex_id.0) {
826            debug_assert_eq!(
827                self.vertex_ids[pos], vertex_id.0,
828                "vertex_pos out of sync with vertex_ids at position {pos}"
829            );
830            self.coords[pos] = new_coord;
831            // Force rebuild on next access to maintain sort invariants
832            self.delta.mark_dirty();
833        }
834    }
835
836    /// Return a copy of `adjacency` extended with the current base+delta
837    /// out-edges of every existing vertex that the input does not cover.
838    ///
839    /// Named-range pass-through vertices (NamedScalar/NamedArray) emit edges
840    /// to their underlying cells via `add_edge` during load; those edges live
841    /// in `base`/`delta` but are not part of the formula-target adjacency that
842    /// bulk-ingest's finalize hands to [`build_from_adjacency`]. Feeding the
843    /// raw adjacency straight to that (pure) builder would therefore silently
844    /// drop the pass-through vertices' out-edges, and `build_demand_subgraph`
845    /// could never reach the underlying cells. Callers run this first to merge
846    /// those edges back in, then pass the result to `build_from_adjacency`.
847    ///
848    /// Must be called BEFORE `build_from_adjacency`, which overwrites
849    /// `base`/`delta`/`vertex_ids`.
850    pub fn adjacency_with_carried_forward_edges(
851        &self,
852        mut adjacency: Vec<(u32, Vec<u32>)>,
853    ) -> Vec<(u32, Vec<u32>)> {
854        let covered: FxHashSet<u32> = adjacency.iter().map(|(vid, _)| *vid).collect();
855        // Carry forward base+delta out-edges for ANY existing vertex not in
856        // the new adjacency input. `vertex_ids` already tracks every vertex
857        // allocated so far, including named-range pass-through vertices.
858        for &vid in &self.vertex_ids {
859            if covered.contains(&vid) {
860                continue;
861            }
862            let v = VertexId(vid);
863            // Merge with any pending delta edges for this vertex.
864            let merged = if self.delta.op_count() == 0 {
865                self.base.out_edges(v).to_vec()
866            } else {
867                self.delta.merged_view(&self.base, v)
868            };
869            if !merged.is_empty() {
870                adjacency.push((vid, merged.into_iter().map(|v| v.0).collect()));
871            }
872        }
873        // Also carry forward delta-only additions (additions to vertices that
874        // weren't in vertex_ids yet — e.g., freshly allocated names whose
875        // add_vertex didn't trigger a rebuild). Pure deltas should be rare
876        // here, but include them for completeness.
877        for (&from, adds) in self.delta.additions_iter() {
878            if covered.contains(&from.0) {
879                continue;
880            }
881            if adjacency.iter().any(|(v, _)| *v == from.0) {
882                continue;
883            }
884            let removals: FxHashSet<u32> = self.delta.removals_for(from).map(|v| v.0).collect();
885            let mut base_set: FxHashSet<u32> =
886                self.base.out_edges(from).iter().map(|v| v.0).collect();
887            for r in &removals {
888                base_set.remove(r);
889            }
890            for &add in adds {
891                base_set.insert(add.0);
892            }
893            if !base_set.is_empty() {
894                let mut targets: Vec<u32> = base_set.into_iter().collect();
895                targets.sort_unstable();
896                adjacency.push((from.0, targets));
897            }
898        }
899        adjacency
900    }
901
902    /// Build underlying CSR directly from adjacency and provided coords/ids.
903    /// This replaces the current base and clears the delta slab.
904    ///
905    /// Pure builder: it uses exactly the edges in `adjacency` and does not
906    /// consult the existing `base`/`delta`. To preserve edges for vertices
907    /// absent from `adjacency` (e.g. named-range pass-through vertices), run
908    /// [`adjacency_with_carried_forward_edges`] first and pass its result in.
909    pub fn build_from_adjacency(
910        &mut self,
911        adjacency: Vec<(u32, Vec<u32>)>,
912        coords: Vec<AbsCoord>,
913        vertex_ids: Vec<u32>,
914    ) {
915        self.base = CsrEdges::from_adjacency(adjacency, &coords);
916        self.coords = coords;
917        self.vertex_ids = vertex_ids;
918        self.vertex_pos = self
919            .vertex_ids
920            .iter()
921            .enumerate()
922            .map(|(idx, &id)| (id, idx))
923            .collect();
924        self.delta.clear();
925    }
926}
927
928impl Default for CsrMutableEdges {
929    fn default() -> Self {
930        Self::new()
931    }
932}