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_end_batch_rebuilds_on_coord_change_only() {
315        let mut edges =
316            CsrMutableEdges::with_coords(vec![AbsCoord::new(0, 0), AbsCoord::new(0, 1)]);
317        edges.begin_batch();
318        edges.update_coord(VertexId(0), AbsCoord::new(0, 2));
319        // No ops, only coord changed; end_batch should rebuild due to coord_dirty
320        edges.end_batch();
321        // Smoke: out_edges call should not panic and reflect empty edges
322        assert_eq!(edges.out_edges(VertexId(0)), Vec::<VertexId>::new());
323    }
324}
325
326/// Delta slab for accumulating edge mutations between CSR rebuilds
327///
328/// Provides O(1) edge mutations by tracking additions and removals
329/// separately, merging them with the base CSR on read.
330#[derive(Debug)]
331pub struct DeltaEdgeSlab {
332    /// New edges to add, grouped by source vertex (set semantics to avoid duplicates)
333    additions: FxHashMap<VertexId, FxHashSet<VertexId>>,
334
335    /// Edges to remove, stored as sets for O(1) lookup
336    removals: FxHashMap<VertexId, FxHashSet<VertexId>>,
337
338    /// Total operation count for rebuild threshold
339    op_count: usize,
340
341    /// Flag indicating coordinates have changed and rebuild is needed
342    coord_changed: bool,
343}
344
345impl DeltaEdgeSlab {
346    /// Create a new empty delta slab
347    pub fn new() -> Self {
348        Self {
349            additions: FxHashMap::default(),
350            removals: FxHashMap::default(),
351            op_count: 0,
352            coord_changed: false,
353        }
354    }
355
356    /// Add an edge from source to target
357    pub fn add_edge(&mut self, from: VertexId, to: VertexId) {
358        // Last-op-wins: if previously removed, cancel the removal
359        if let Some(rem) = self.removals.get_mut(&from) {
360            rem.remove(&to);
361        }
362        // Insert into additions set
363        self.additions.entry(from).or_default().insert(to);
364        self.op_count += 1;
365    }
366
367    /// Remove an edge from source to target
368    pub fn remove_edge(&mut self, from: VertexId, to: VertexId) {
369        // Last-op-wins: if previously added in this slab, cancel the addition
370        if let Some(adds) = self.additions.get_mut(&from) {
371            adds.remove(&to);
372        }
373        // Record removal
374        self.removals.entry(from).or_default().insert(to);
375        self.op_count += 1;
376    }
377
378    /// Get a merged view of edges for a vertex, combining CSR and delta
379    pub fn merged_view(&self, csr: &CsrEdges, v: VertexId) -> Vec<VertexId> {
380        // Start from base CSR out-edges
381        let mut result: Vec<_> = csr.out_edges(v).to_vec();
382        // Remove edges marked for deletion
383        if let Some(removes) = self.removals.get(&v) {
384            result.retain(|e| !removes.contains(e));
385        }
386        // Add new edges (set semantics)
387        if let Some(adds) = self.additions.get(&v) {
388            result.extend(adds.iter().copied());
389        }
390        // Dedup deterministically and sort by VertexId for stable order
391        let mut seen: FxHashSet<VertexId> = FxHashSet::default();
392        result.retain(|e| seen.insert(*e));
393        result.sort_by_key(|e| e.0);
394        result
395    }
396
397    /// Check if the delta needs to be applied (rebuild threshold reached)
398    pub fn needs_rebuild(&self) -> bool {
399        self.op_count >= 1000 || self.coord_changed
400    }
401
402    /// Mark that coordinates have changed and rebuild is needed
403    pub fn mark_dirty(&mut self) {
404        self.coord_changed = true;
405    }
406
407    /// Get the current operation count
408    pub fn op_count(&self) -> usize {
409        self.op_count
410    }
411
412    /// Clear the delta slab
413    pub fn clear(&mut self) {
414        self.additions.clear();
415        self.removals.clear();
416        self.op_count = 0;
417        self.coord_changed = false;
418    }
419
420    /// Iterate over all (from, &additions) pairs in the delta. Used by
421    /// build_from_adjacency to carry forward delta-only edges that the
422    /// adjacency input does not cover.
423    pub fn additions_iter(&self) -> impl Iterator<Item = (&VertexId, &FxHashSet<VertexId>)> {
424        self.additions.iter()
425    }
426
427    /// Return removals scheduled for `from` (empty slice if none).
428    pub fn removals_for(&self, from: VertexId) -> impl Iterator<Item = VertexId> + '_ {
429        self.removals
430            .get(&from)
431            .into_iter()
432            .flat_map(|set| set.iter().copied())
433    }
434
435    /// Apply delta to CSR, creating a new CSR structure
436    pub fn apply_to_csr(
437        &self,
438        base: &CsrEdges,
439        coords: &[AbsCoord],
440        vertex_ids: &[u32],
441    ) -> CsrEdges {
442        let mut adjacency = Vec::with_capacity(vertex_ids.len());
443
444        // Build new adjacency list by merging base and delta
445        for &vid in vertex_ids {
446            let v = VertexId(vid);
447            let merged = self.merged_view(base, v);
448
449            // Convert to u32 for adjacency format
450            let targets: Vec<u32> = merged.into_iter().map(|id| id.0).collect();
451
452            adjacency.push((vid, targets));
453        }
454
455        CsrEdges::from_adjacency(adjacency, coords)
456    }
457}
458
459impl Default for DeltaEdgeSlab {
460    fn default() -> Self {
461        Self::new()
462    }
463}
464
465/// Mutable edge storage combining CSR base with delta slab
466///
467/// Provides efficient edge mutations with automatic rebuild when
468/// delta grows too large.
469#[derive(Debug)]
470pub struct CsrMutableEdges {
471    /// Base CSR structure (immutable between rebuilds)
472    base: CsrEdges,
473
474    /// Delta slab for mutations
475    delta: DeltaEdgeSlab,
476
477    /// Vertex coordinates for deterministic ordering
478    coords: Vec<AbsCoord>,
479
480    /// Vertex IDs corresponding to coords array
481    vertex_ids: Vec<u32>,
482
483    /// Position of each vertex id in the coords and vertex_ids arrays.
484    vertex_pos: FxHashMap<u32, usize>,
485
486    /// Nested batch depth; non-zero defers automatic rebuilds.
487    batch_depth: usize,
488}
489
490impl CsrMutableEdges {
491    /// Create new mutable edges with empty base
492    pub fn new() -> Self {
493        Self {
494            base: CsrEdges::empty(),
495            delta: DeltaEdgeSlab::new(),
496            coords: Vec::new(),
497            vertex_ids: Vec::new(),
498            vertex_pos: FxHashMap::default(),
499            batch_depth: 0,
500        }
501    }
502
503    /// Create with initial vertex coordinates
504    pub fn with_coords(coords: Vec<AbsCoord>) -> Self {
505        let num_vertices = coords.len();
506        let vertex_ids: Vec<u32> = (0..num_vertices as u32).collect();
507        let adjacency: Vec<_> = vertex_ids.iter().map(|&id| (id, Vec::new())).collect();
508        let vertex_pos = vertex_ids
509            .iter()
510            .enumerate()
511            .map(|(idx, &id)| (id, idx))
512            .collect();
513
514        Self {
515            base: CsrEdges::from_adjacency(adjacency, &coords),
516            delta: DeltaEdgeSlab::new(),
517            coords,
518            vertex_ids,
519            vertex_pos,
520            batch_depth: 0,
521        }
522    }
523
524    /// Add an edge, rebuilding if threshold reached
525    pub fn add_edge(&mut self, from: VertexId, to: VertexId) {
526        self.delta.add_edge(from, to);
527        self.maybe_rebuild();
528    }
529
530    /// Remove an edge, rebuilding if threshold reached
531    pub fn remove_edge(&mut self, from: VertexId, to: VertexId) {
532        self.delta.remove_edge(from, to);
533        self.maybe_rebuild();
534    }
535
536    /// Get outgoing edges for a vertex (merged view)
537    pub fn out_edges(&self, v: VertexId) -> Vec<VertexId> {
538        if self.delta.op_count() == 0 {
539            self.base.out_edges(v).to_vec()
540        } else {
541            self.delta.merged_view(&self.base, v)
542        }
543    }
544
545    /// Borrow outgoing edges when no delta mutations are pending.
546    ///
547    /// This is a zero-allocation hot path for read-heavy evaluation/scheduling phases.
548    #[inline]
549    pub fn out_edges_ref(&self, v: VertexId) -> Option<&[VertexId]> {
550        if self.delta.op_count() == 0 {
551            Some(self.base.out_edges(v))
552        } else {
553            None
554        }
555    }
556
557    /// Get incoming edges from base CSR (delta not applied for performance)
558    /// After rebuild, this will include all changes
559    pub fn in_edges(&self, v: VertexId) -> &[VertexId] {
560        self.base.in_edges(v)
561    }
562
563    /// Borrow incoming edges when no delta mutations are pending.
564    ///
565    /// This is a zero-allocation hot path for read-heavy evaluation/scheduling phases.
566    #[inline]
567    pub fn in_edges_ref(&self, v: VertexId) -> Option<&[VertexId]> {
568        if self.delta.op_count() == 0 {
569            Some(self.base.in_edges(v))
570        } else {
571            None
572        }
573    }
574
575    /// Get the current delta size
576    pub fn delta_size(&self) -> usize {
577        self.delta.op_count()
578    }
579
580    /// Return the exact number of logical outgoing dependency edges, including pending delta
581    /// mutations.
582    ///
583    /// This is intended for read-only observability. When the delta slab is non-empty, the
584    /// implementation walks the known vertex ids and merges each outgoing edge list, so callers
585    /// should avoid putting it on hot evaluation paths.
586    pub fn num_edges_exact(&self) -> usize {
587        if self.delta.op_count() == 0 {
588            return self.base.num_edges();
589        }
590
591        self.vertex_ids
592            .iter()
593            .map(|&id| self.out_edges(VertexId(id)).len())
594            .sum()
595    }
596
597    /// Force a rebuild of the CSR structure
598    pub fn rebuild(&mut self) {
599        if self.delta.op_count() > 0 || self.delta.needs_rebuild() {
600            self.base = self
601                .delta
602                .apply_to_csr(&self.base, &self.coords, &self.vertex_ids);
603            self.delta.clear();
604        }
605    }
606
607    /// Check and perform rebuild if threshold reached
608    fn maybe_rebuild(&mut self) {
609        if self.batch_depth == 0 && self.delta.needs_rebuild() {
610            self.rebuild();
611        }
612    }
613
614    /// Enter batch mode - defer rebuilds until the outer end_batch() call.
615    pub fn begin_batch(&mut self) {
616        self.batch_depth = self.batch_depth.saturating_add(1);
617    }
618
619    /// Exit batch mode and rebuild if needed.
620    pub fn end_batch(&mut self) {
621        self.batch_depth = self.batch_depth.saturating_sub(1);
622        if self.batch_depth == 0 && (self.delta.op_count() > 0 || self.delta.needs_rebuild()) {
623            self.rebuild();
624        }
625    }
626
627    /// Add a new vertex with its coordinate and ID
628    pub fn add_vertex(&mut self, coord: AbsCoord, vertex_id: u32) -> usize {
629        let idx = self.coords.len();
630        self.coords.push(coord);
631        self.vertex_ids.push(vertex_id);
632        self.vertex_pos.insert(vertex_id, idx);
633
634        // Rebuild base to include new vertex
635        // This is necessary to maintain CSR structure consistency
636        self.rebuild();
637
638        idx
639    }
640
641    /// Add many vertices at once; single rebuild at end.
642    pub fn add_vertices_batch(&mut self, items: &[(AbsCoord, u32)]) {
643        if items.is_empty() {
644            return;
645        }
646        let start_len = self.coords.len();
647        self.coords.reserve(items.len());
648        self.vertex_ids.reserve(items.len());
649        for (coord, vid) in items {
650            let idx = self.coords.len();
651            self.coords.push(*coord);
652            self.vertex_ids.push(*vid);
653            self.vertex_pos.insert(*vid, idx);
654        }
655        // Single rebuild to incorporate all new vertices.
656        self.rebuild();
657        debug_assert_eq!(self.coords.len(), start_len + items.len());
658    }
659
660    /// Update coordinate for a vertex in the cache
661    /// Marks for rebuild to maintain sort order
662    pub fn update_coord(&mut self, vertex_id: VertexId, new_coord: AbsCoord) {
663        if let Some(&pos) = self.vertex_pos.get(&vertex_id.0) {
664            debug_assert_eq!(
665                self.vertex_ids[pos], vertex_id.0,
666                "vertex_pos out of sync with vertex_ids at position {pos}"
667            );
668            self.coords[pos] = new_coord;
669            // Force rebuild on next access to maintain sort invariants
670            self.delta.mark_dirty();
671        }
672    }
673
674    /// Return a copy of `adjacency` extended with the current base+delta
675    /// out-edges of every existing vertex that the input does not cover.
676    ///
677    /// Named-range pass-through vertices (NamedScalar/NamedArray) emit edges
678    /// to their underlying cells via `add_edge` during load; those edges live
679    /// in `base`/`delta` but are not part of the formula-target adjacency that
680    /// bulk-ingest's finalize hands to [`build_from_adjacency`]. Feeding the
681    /// raw adjacency straight to that (pure) builder would therefore silently
682    /// drop the pass-through vertices' out-edges, and `build_demand_subgraph`
683    /// could never reach the underlying cells. Callers run this first to merge
684    /// those edges back in, then pass the result to `build_from_adjacency`.
685    ///
686    /// Must be called BEFORE `build_from_adjacency`, which overwrites
687    /// `base`/`delta`/`vertex_ids`.
688    pub fn adjacency_with_carried_forward_edges(
689        &self,
690        mut adjacency: Vec<(u32, Vec<u32>)>,
691    ) -> Vec<(u32, Vec<u32>)> {
692        let covered: FxHashSet<u32> = adjacency.iter().map(|(vid, _)| *vid).collect();
693        // Carry forward base+delta out-edges for ANY existing vertex not in
694        // the new adjacency input. `vertex_ids` already tracks every vertex
695        // allocated so far, including named-range pass-through vertices.
696        for &vid in &self.vertex_ids {
697            if covered.contains(&vid) {
698                continue;
699            }
700            let v = VertexId(vid);
701            // Merge with any pending delta edges for this vertex.
702            let merged = if self.delta.op_count() == 0 {
703                self.base.out_edges(v).to_vec()
704            } else {
705                self.delta.merged_view(&self.base, v)
706            };
707            if !merged.is_empty() {
708                adjacency.push((vid, merged.into_iter().map(|v| v.0).collect()));
709            }
710        }
711        // Also carry forward delta-only additions (additions to vertices that
712        // weren't in vertex_ids yet — e.g., freshly allocated names whose
713        // add_vertex didn't trigger a rebuild). Pure deltas should be rare
714        // here, but include them for completeness.
715        for (&from, adds) in self.delta.additions_iter() {
716            if covered.contains(&from.0) {
717                continue;
718            }
719            if adjacency.iter().any(|(v, _)| *v == from.0) {
720                continue;
721            }
722            let removals: FxHashSet<u32> = self.delta.removals_for(from).map(|v| v.0).collect();
723            let mut base_set: FxHashSet<u32> =
724                self.base.out_edges(from).iter().map(|v| v.0).collect();
725            for r in &removals {
726                base_set.remove(r);
727            }
728            for &add in adds {
729                base_set.insert(add.0);
730            }
731            if !base_set.is_empty() {
732                let mut targets: Vec<u32> = base_set.into_iter().collect();
733                targets.sort_unstable();
734                adjacency.push((from.0, targets));
735            }
736        }
737        adjacency
738    }
739
740    /// Build underlying CSR directly from adjacency and provided coords/ids.
741    /// This replaces the current base and clears the delta slab.
742    ///
743    /// Pure builder: it uses exactly the edges in `adjacency` and does not
744    /// consult the existing `base`/`delta`. To preserve edges for vertices
745    /// absent from `adjacency` (e.g. named-range pass-through vertices), run
746    /// [`adjacency_with_carried_forward_edges`] first and pass its result in.
747    pub fn build_from_adjacency(
748        &mut self,
749        adjacency: Vec<(u32, Vec<u32>)>,
750        coords: Vec<AbsCoord>,
751        vertex_ids: Vec<u32>,
752    ) {
753        self.base = CsrEdges::from_adjacency(adjacency, &coords);
754        self.coords = coords;
755        self.vertex_ids = vertex_ids;
756        self.vertex_pos = self
757            .vertex_ids
758            .iter()
759            .enumerate()
760            .map(|(idx, &id)| (id, idx))
761            .collect();
762        self.delta.clear();
763    }
764}
765
766impl Default for CsrMutableEdges {
767    fn default() -> Self {
768        Self::new()
769    }
770}