formualizer_eval/engine/graph/
mod.rs

1use crate::SheetId;
2use crate::engine::named_range::{NameScope, NamedDefinition, NamedRange};
3use crate::engine::sheet_registry::SheetRegistry;
4use formualizer_common::{ExcelError, ExcelErrorKind, LiteralValue};
5use formualizer_parse::parser::{ASTNode, ASTNodeType, ReferenceType};
6use rustc_hash::{FxHashMap, FxHashSet};
7
8#[cfg(test)]
9#[derive(Debug, Default, Clone)]
10pub struct GraphInstrumentation {
11    pub edges_added: u64,
12    pub stripe_inserts: u64,
13    pub stripe_removes: u64,
14}
15
16// Type alias for complex return type
17type ExtractDependenciesResult =
18    Result<(Vec<VertexId>, Vec<ReferenceType>, Vec<CellRef>), ExcelError>;
19
20pub mod editor;
21pub mod snapshot;
22
23use super::arena::{AstNodeId, DataStore, ValueRef};
24use super::delta_edges::CsrMutableEdges;
25use super::packed_coord::PackedCoord;
26use super::sheet_index::SheetIndex;
27use super::vertex::{VertexId, VertexKind};
28use super::vertex_store::{FIRST_NORMAL_VERTEX, VertexStore};
29use crate::engine::topo::{
30    GraphAdapter,
31    pk::{DynamicTopo, PkConfig},
32};
33use crate::reference::{CellRef, Coord};
34// topo::pk wiring will be integrated behind config.use_dynamic_topo in a follow-up step
35
36pub use editor::change_log::{ChangeEvent, ChangeLog};
37
38// ChangeEvent is now imported from change_log module
39
40/// đź”® Scalability Hook: Dependency reference types for range compression
41#[derive(Debug, Clone, PartialEq, Eq, Hash)]
42pub enum DependencyRef {
43    /// A specific cell dependency
44    Cell(VertexId),
45    /// A dependency on a finite, rectangular range
46    Range {
47        sheet: String,
48        start_row: u32,
49        start_col: u32,
50        end_row: u32, // Inclusive
51        end_col: u32, // Inclusive
52    },
53    /// A whole column dependency (A:A) - future range compression
54    WholeColumn { sheet: String, col: u32 },
55    /// A whole row dependency (1:1) - future range compression  
56    WholeRow { sheet: String, row: u32 },
57}
58
59/// A key representing a coarse-grained section of a sheet
60#[derive(Debug, Clone, Hash, PartialEq, Eq)]
61pub struct StripeKey {
62    pub sheet_id: SheetId,
63    pub stripe_type: StripeType,
64    pub index: u32, // The index of the row, column, or block stripe
65}
66
67#[derive(Debug, Clone, Hash, PartialEq, Eq)]
68pub enum StripeType {
69    Row,
70    Column,
71    Block, // For dense, square-like ranges
72}
73
74/// Block stripe indexing mathematics
75const BLOCK_H: u32 = 256;
76const BLOCK_W: u32 = 256;
77
78pub fn block_index(row: u32, col: u32) -> u32 {
79    (row / BLOCK_H) << 16 | (col / BLOCK_W)
80}
81
82/// Validate that a name conforms to Excel naming rules
83fn is_valid_excel_name(name: &str) -> bool {
84    // Excel name rules:
85    // 1. Must start with a letter, underscore, or backslash
86    // 2. Can contain letters, numbers, periods, and underscores
87    // 3. Cannot be a cell reference (like A1, B2, etc.)
88    // 4. Cannot exceed 255 characters
89    // 5. Cannot contain spaces
90
91    if name.is_empty() || name.len() > 255 {
92        return false;
93    }
94
95    // Check if it looks like a cell reference (basic check)
96    if is_cell_reference(name) {
97        return false;
98    }
99
100    let mut chars = name.chars();
101
102    // First character must be letter, underscore, or backslash
103    if let Some(first) = chars.next() {
104        if !first.is_alphabetic() && first != '_' && first != '\\' {
105            return false;
106        }
107    }
108
109    // Remaining characters must be letters, digits, periods, or underscores
110    for c in chars {
111        if !c.is_alphanumeric() && c != '.' && c != '_' {
112            return false;
113        }
114    }
115
116    true
117}
118
119/// Check if a string looks like a cell reference
120fn is_cell_reference(s: &str) -> bool {
121    // A cell reference must:
122    // 1. Start with optional $, then 1-3 letters (column)
123    // 2. Followed by optional $, then digits (row)
124    // Examples: A1, $A$1, AB123, $XFD$1048576
125
126    let s = s.trim_start_matches('$');
127
128    // Find where letters end and digits begin
129    let letter_end = s.chars().position(|c| !c.is_alphabetic() && c != '$');
130
131    if let Some(pos) = letter_end {
132        let (col_part, rest) = s.split_at(pos);
133
134        // Column part must be 1-3 letters
135        if col_part.is_empty() || col_part.len() > 3 {
136            return false;
137        }
138
139        // Check if all are letters
140        if !col_part.chars().all(|c| c.is_alphabetic()) {
141            return false;
142        }
143
144        // Rest must be optional $ followed by digits only
145        let row_part = rest.trim_start_matches('$');
146
147        // Must have at least one digit and all must be digits
148        !row_part.is_empty() && row_part.chars().all(|c| c.is_ascii_digit())
149    } else {
150        // No digits found, not a cell reference
151        false
152    }
153}
154
155/// A summary of the results of a mutating operation on the graph.
156/// This serves as a "changelog" to the application layer.
157#[derive(Debug, Clone)]
158pub struct OperationSummary {
159    /// Vertices whose values have been directly or indirectly affected.
160    pub affected_vertices: Vec<VertexId>,
161    /// Placeholder cells that were newly created to satisfy dependencies.
162    pub created_placeholders: Vec<CellRef>,
163}
164
165/// SoA-based dependency graph implementation
166#[derive(Debug)]
167pub struct DependencyGraph {
168    // Core columnar storage
169    store: VertexStore,
170
171    // Edge storage with delta slab
172    edges: CsrMutableEdges,
173
174    // Arena-based value and formula storage
175    data_store: DataStore,
176    vertex_values: FxHashMap<VertexId, ValueRef>,
177    vertex_formulas: FxHashMap<VertexId, AstNodeId>,
178
179    // Address mappings using fast hashing
180    cell_to_vertex: FxHashMap<CellRef, VertexId>,
181
182    // Scheduling state - using HashSet for O(1) operations
183    dirty_vertices: FxHashSet<VertexId>,
184    volatile_vertices: FxHashSet<VertexId>,
185
186    // NEW: Specialized managers for range dependencies (Hybrid Model)
187    /// Maps a formula vertex to the ranges it depends on.
188    formula_to_range_deps: FxHashMap<VertexId, Vec<ReferenceType>>,
189
190    /// Maps a stripe to formulas that depend on it via a compressed range.
191    /// CRITICAL: VertexIds are deduplicated within each stripe to avoid quadratic blow-ups.
192    stripe_to_dependents: FxHashMap<StripeKey, FxHashSet<VertexId>>,
193
194    // Sheet-level sparse indexes for O(log n + k) range queries
195    /// Maps sheet_id to its interval tree index for efficient row/column operations
196    sheet_indexes: FxHashMap<SheetId, SheetIndex>,
197
198    // Sheet name/ID mapping
199    sheet_reg: SheetRegistry,
200    default_sheet_id: SheetId,
201
202    // Named ranges support
203    /// Workbook-scoped named ranges
204    named_ranges: FxHashMap<String, NamedRange>,
205
206    /// Sheet-scoped named ranges  
207    sheet_named_ranges: FxHashMap<(SheetId, String), NamedRange>,
208
209    /// Reverse mapping: vertex -> names it uses
210    vertex_to_names: FxHashMap<VertexId, Vec<String>>,
211
212    // Evaluation configuration
213    config: super::EvalConfig,
214
215    // Dynamic topology orderer (Pearce–Kelly) maintained alongside edges when enabled
216    pk_order: Option<DynamicTopo<VertexId>>,
217
218    // Spill registry: anchor -> cells, and reverse mapping for blockers
219    spill_anchor_to_cells: FxHashMap<VertexId, Vec<CellRef>>,
220    spill_cell_to_anchor: FxHashMap<CellRef, VertexId>,
221
222    // Hint: during initial bulk load, many cells are guaranteed new; allow skipping existence checks per-sheet
223    first_load_assume_new: bool,
224    ensure_touched_sheets: FxHashSet<SheetId>,
225
226    #[cfg(test)]
227    instr: GraphInstrumentation,
228}
229
230impl Default for DependencyGraph {
231    fn default() -> Self {
232        Self::new()
233    }
234}
235
236impl DependencyGraph {
237    /// Expose range expansion limit for planners
238    pub fn range_expansion_limit(&self) -> usize {
239        self.config.range_expansion_limit
240    }
241
242    pub fn get_config(&self) -> &super::EvalConfig {
243        &self.config
244    }
245
246    /// Build a dependency plan for a set of formulas on sheets
247    pub fn plan_dependencies<'a, I>(
248        &mut self,
249        items: I,
250        policy: &formualizer_parse::parser::CollectPolicy,
251        volatile: Option<&[bool]>,
252    ) -> Result<crate::engine::plan::DependencyPlan, formualizer_common::ExcelError>
253    where
254        I: IntoIterator<Item = (&'a str, u32, u32, &'a formualizer_parse::parser::ASTNode)>,
255    {
256        crate::engine::plan::build_dependency_plan(
257            &mut self.sheet_reg,
258            items.into_iter(),
259            policy,
260            volatile,
261        )
262    }
263
264    /// Ensure vertices exist for given coords; allocate missing in contiguous batches and add to edges/index.
265    /// Returns a list suitable for edges.add_vertices_batch.
266    pub fn ensure_vertices_batch(
267        &mut self,
268        coords: &[(SheetId, PackedCoord)],
269    ) -> Vec<(PackedCoord, u32)> {
270        use rustc_hash::FxHashMap;
271        let mut grouped: FxHashMap<SheetId, Vec<PackedCoord>> = FxHashMap::default();
272        for (sid, pc) in coords.iter().copied() {
273            let addr = CellRef::new(sid, Coord::new(pc.row(), pc.col(), true, true));
274            if self.cell_to_vertex.contains_key(&addr) {
275                continue;
276            }
277            grouped.entry(sid).or_default().push(pc);
278        }
279        let mut add_batch: Vec<(PackedCoord, u32)> = Vec::new();
280        for (sid, pcs) in grouped {
281            if pcs.is_empty() {
282                continue;
283            }
284            // Mark sheet as touched by ensure to disable fast-path new allocations for its values
285            self.ensure_touched_sheets.insert(sid);
286            let vids = self.store.allocate_contiguous(sid, &pcs, 0x00);
287            for (i, pc) in pcs.iter().enumerate() {
288                let vid = vids[i];
289                add_batch.push((*pc, vid.0));
290                let addr = CellRef::new(sid, Coord::new(pc.row(), pc.col(), true, true));
291                self.cell_to_vertex.insert(addr, vid);
292                // Respect sheet index mode: skip index updates in Lazy mode during bulk ensure
293                match self.config.sheet_index_mode {
294                    crate::engine::SheetIndexMode::Eager
295                    | crate::engine::SheetIndexMode::FastBatch => {
296                        self.sheet_index_mut(sid).add_vertex(*pc, vid);
297                    }
298                    crate::engine::SheetIndexMode::Lazy => {
299                        // defer index build
300                    }
301                }
302            }
303        }
304        if !add_batch.is_empty() {
305            self.edges.add_vertices_batch(&add_batch);
306        }
307        add_batch
308    }
309
310    /// Enable/disable the first-load fast path for value inserts.
311    pub fn set_first_load_assume_new(&mut self, enabled: bool) {
312        self.first_load_assume_new = enabled;
313    }
314
315    /// Reset the per-sheet ensure touch tracking.
316    pub fn reset_ensure_touched(&mut self) {
317        self.ensure_touched_sheets.clear();
318    }
319
320    /// Store ASTs in batch and return their arena ids
321    pub fn store_asts_batch<'a, I>(&mut self, asts: I) -> Vec<AstNodeId>
322    where
323        I: IntoIterator<Item = &'a formualizer_parse::parser::ASTNode>,
324    {
325        self.data_store.store_asts_batch(asts, &self.sheet_reg)
326    }
327
328    /// Lookup VertexId for a (SheetId, PackedCoord)
329    pub fn vid_for_sid_pc(&self, sid: SheetId, pc: PackedCoord) -> Option<VertexId> {
330        let addr = CellRef::new(sid, Coord::new(pc.row(), pc.col(), true, true));
331        self.cell_to_vertex.get(&addr).copied()
332    }
333
334    /// Helper to map a global cell index in a plan to a VertexId
335    pub fn vid_for_plan_idx(
336        &self,
337        plan: &crate::engine::plan::DependencyPlan,
338        idx: u32,
339    ) -> Option<VertexId> {
340        let (sid, pc) = plan.global_cells.get(idx as usize).copied()?;
341        self.vid_for_sid_pc(sid, pc)
342    }
343
344    /// Assign a formula to an existing vertex, removing prior edges and setting flags
345    pub fn assign_formula_vertex(&mut self, vid: VertexId, ast_id: AstNodeId, volatile: bool) {
346        if self.vertex_formulas.contains_key(&vid) {
347            self.remove_dependent_edges(vid);
348        }
349        self.store
350            .set_kind(vid, crate::engine::vertex::VertexKind::FormulaScalar);
351        self.vertex_values.remove(&vid);
352        self.vertex_formulas.insert(vid, ast_id);
353        if volatile {
354            self.store.set_volatile(vid, true);
355            self.volatile_vertices.insert(vid);
356        }
357        // schedule evaluation
358        self.mark_vertex_dirty(vid);
359    }
360
361    /// Public wrapper for adding edges without beginning a batch (caller manages batch)
362    pub fn add_edges_nobatch(&mut self, dependent: VertexId, dependencies: &[VertexId]) {
363        self.add_dependent_edges_nobatch(dependent, dependencies);
364    }
365
366    /// Public wrapper to add range-dependent edges
367    pub fn add_range_edges(
368        &mut self,
369        dependent: VertexId,
370        ranges: &[ReferenceType],
371        current_sheet_id: SheetId,
372    ) {
373        self.add_range_dependent_edges(dependent, ranges, current_sheet_id);
374    }
375
376    /// Iterate all normal vertex ids
377    pub fn iter_vertex_ids(&self) -> impl Iterator<Item = VertexId> + '_ {
378        self.store.all_vertices()
379    }
380
381    /// Get current PackedCoord for a vertex
382    pub fn vertex_coord(&self, vid: VertexId) -> PackedCoord {
383        self.store.coord(vid)
384    }
385
386    /// Total number of allocated vertices (including deleted)
387    pub fn vertex_count(&self) -> usize {
388        self.store.len()
389    }
390
391    /// Replace CSR edges in one shot from adjacency and coords
392    pub fn build_edges_from_adjacency(
393        &mut self,
394        adjacency: Vec<(u32, Vec<u32>)>,
395        coords: Vec<PackedCoord>,
396        vertex_ids: Vec<u32>,
397    ) {
398        self.edges
399            .build_from_adjacency(adjacency, coords, vertex_ids);
400    }
401    /// Compute min/max used row among vertices within [start_col..=end_col] on a sheet.
402    pub fn used_row_bounds_for_columns(
403        &self,
404        sheet_id: SheetId,
405        start_col: u32,
406        end_col: u32,
407    ) -> Option<(u32, u32)> {
408        // Prefer sheet index when available
409        if let Some(index) = self.sheet_indexes.get(&sheet_id) {
410            if !index.is_empty() {
411                let mut min_r: Option<u32> = None;
412                let mut max_r: Option<u32> = None;
413                for vid in index.vertices_in_col_range(start_col, end_col) {
414                    let r = self.store.coord(vid).row();
415                    min_r = Some(min_r.map(|m| m.min(r)).unwrap_or(r));
416                    max_r = Some(max_r.map(|m| m.max(r)).unwrap_or(r));
417                }
418                return match (min_r, max_r) {
419                    (Some(a), Some(b)) => Some((a, b)),
420                    _ => None,
421                };
422            }
423        }
424        // Fallback: scan cell map for bounds on the fly
425        let mut min_r: Option<u32> = None;
426        let mut max_r: Option<u32> = None;
427        for cref in self.cell_to_vertex.keys() {
428            if cref.sheet_id == sheet_id {
429                let c = cref.coord.col;
430                if c >= start_col && c <= end_col {
431                    let r = cref.coord.row;
432                    min_r = Some(min_r.map(|m| m.min(r)).unwrap_or(r));
433                    max_r = Some(max_r.map(|m| m.max(r)).unwrap_or(r));
434                }
435            }
436        }
437        match (min_r, max_r) {
438            (Some(a), Some(b)) => Some((a, b)),
439            _ => None,
440        }
441    }
442
443    /// Build (or rebuild) the sheet index for a given sheet if running in Lazy mode.
444    pub fn finalize_sheet_index(&mut self, sheet: &str) {
445        let Some(sheet_id) = self.sheet_reg.get_id(sheet) else {
446            return;
447        };
448        // If already present and non-empty, skip
449        if let Some(idx) = self.sheet_indexes.get(&sheet_id) {
450            if !idx.is_empty() {
451                return;
452            }
453        }
454        let mut idx = SheetIndex::new();
455        // Collect coords for this sheet
456        let mut batch: Vec<(PackedCoord, VertexId)> = Vec::with_capacity(self.cell_to_vertex.len());
457        for (cref, vid) in &self.cell_to_vertex {
458            if cref.sheet_id == sheet_id {
459                batch.push((PackedCoord::new(cref.coord.row, cref.coord.col), *vid));
460            }
461        }
462        // Use batch builder
463        idx.add_vertices_batch(&batch);
464        self.sheet_indexes.insert(sheet_id, idx);
465    }
466
467    pub fn set_sheet_index_mode(&mut self, mode: crate::engine::SheetIndexMode) {
468        self.config.sheet_index_mode = mode;
469    }
470
471    /// Compute min/max used column among vertices within [start_row..=end_row] on a sheet.
472    pub fn used_col_bounds_for_rows(
473        &self,
474        sheet_id: SheetId,
475        start_row: u32,
476        end_row: u32,
477    ) -> Option<(u32, u32)> {
478        if let Some(index) = self.sheet_indexes.get(&sheet_id) {
479            if !index.is_empty() {
480                let mut min_c: Option<u32> = None;
481                let mut max_c: Option<u32> = None;
482                for vid in index.vertices_in_row_range(start_row, end_row) {
483                    let c = self.store.coord(vid).col();
484                    min_c = Some(min_c.map(|m| m.min(c)).unwrap_or(c));
485                    max_c = Some(max_c.map(|m| m.max(c)).unwrap_or(c));
486                }
487                return match (min_c, max_c) {
488                    (Some(a), Some(b)) => Some((a, b)),
489                    _ => None,
490                };
491            }
492        }
493        // Fallback: scan cell map on the fly
494        let mut min_c: Option<u32> = None;
495        let mut max_c: Option<u32> = None;
496        for cref in self.cell_to_vertex.keys() {
497            if cref.sheet_id == sheet_id {
498                let r = cref.coord.row;
499                if r >= start_row && r <= end_row {
500                    let c = cref.coord.col;
501                    min_c = Some(min_c.map(|m| m.min(c)).unwrap_or(c));
502                    max_c = Some(max_c.map(|m| m.max(c)).unwrap_or(c));
503                }
504            }
505        }
506        match (min_c, max_c) {
507            (Some(a), Some(b)) => Some((a, b)),
508            _ => None,
509        }
510    }
511
512    /// Returns true if the given sheet currently contains any formula vertices.
513    pub fn sheet_has_formulas(&self, sheet_id: SheetId) -> bool {
514        // Check vertex_formulas keys; they represent formula vertices
515        for &vid in self.vertex_formulas.keys() {
516            if self.store.sheet_id(vid) == sheet_id {
517                return true;
518            }
519        }
520        false
521    }
522    pub fn new() -> Self {
523        let mut sheet_reg = SheetRegistry::new();
524        let default_sheet_id = sheet_reg.id_for("Sheet1");
525        Self {
526            store: VertexStore::new(),
527            edges: CsrMutableEdges::new(),
528            data_store: DataStore::new(),
529            vertex_values: FxHashMap::default(),
530            vertex_formulas: FxHashMap::default(),
531            cell_to_vertex: FxHashMap::default(),
532            dirty_vertices: FxHashSet::default(),
533            volatile_vertices: FxHashSet::default(),
534            formula_to_range_deps: FxHashMap::default(),
535            stripe_to_dependents: FxHashMap::default(),
536            sheet_indexes: FxHashMap::default(),
537            sheet_reg,
538            default_sheet_id,
539            named_ranges: FxHashMap::default(),
540            sheet_named_ranges: FxHashMap::default(),
541            vertex_to_names: FxHashMap::default(),
542            config: super::EvalConfig::default(),
543            pk_order: None,
544            spill_anchor_to_cells: FxHashMap::default(),
545            spill_cell_to_anchor: FxHashMap::default(),
546            first_load_assume_new: false,
547            ensure_touched_sheets: FxHashSet::default(),
548            #[cfg(test)]
549            instr: GraphInstrumentation::default(),
550        }
551    }
552
553    pub fn new_with_config(config: super::EvalConfig) -> Self {
554        let mut g = Self {
555            config: config.clone(),
556            ..Self::new()
557        };
558        if config.use_dynamic_topo {
559            // Seed with currently active vertices (likely empty at startup)
560            let nodes = g
561                .store
562                .all_vertices()
563                .filter(|&id| g.store.vertex_exists_active(id));
564            let mut pk = DynamicTopo::new(
565                nodes,
566                PkConfig {
567                    visit_budget: config.pk_visit_budget,
568                    compaction_interval_ops: config.pk_compaction_interval_ops,
569                },
570            );
571            // Build an initial order using current graph
572            let adapter = GraphAdapter { g: &g };
573            pk.rebuild_full(&adapter);
574            g.pk_order = Some(pk);
575        }
576        g
577    }
578
579    /// When dynamic topology is enabled, compute layers for a subset using PK ordering.
580    pub(crate) fn pk_layers_for(&self, subset: &[VertexId]) -> Option<Vec<crate::engine::Layer>> {
581        let pk = self.pk_order.as_ref()?;
582        let adapter = crate::engine::topo::GraphAdapter { g: self };
583        let layers = pk.layers_for(&adapter, subset, self.config.max_layer_width);
584        Some(
585            layers
586                .into_iter()
587                .map(|vs| crate::engine::Layer { vertices: vs })
588                .collect(),
589        )
590    }
591
592    #[inline]
593    pub(crate) fn dynamic_topo_enabled(&self) -> bool {
594        self.pk_order.is_some()
595    }
596
597    #[cfg(test)]
598    pub fn reset_instr(&mut self) {
599        self.instr = GraphInstrumentation::default();
600    }
601
602    #[cfg(test)]
603    pub fn instr(&self) -> GraphInstrumentation {
604        self.instr.clone()
605    }
606
607    /// Begin batch operations - defer CSR rebuilds until end_batch() is called
608    pub fn begin_batch(&mut self) {
609        self.edges.begin_batch();
610    }
611
612    /// End batch operations and trigger CSR rebuild if needed
613    pub fn end_batch(&mut self) {
614        self.edges.end_batch();
615    }
616
617    pub fn default_sheet_id(&self) -> SheetId {
618        self.default_sheet_id
619    }
620
621    pub fn default_sheet_name(&self) -> &str {
622        self.sheet_reg.name(self.default_sheet_id)
623    }
624
625    pub fn set_default_sheet_by_name(&mut self, name: &str) {
626        self.default_sheet_id = self.sheet_id_mut(name);
627    }
628
629    pub fn set_default_sheet_by_id(&mut self, id: SheetId) {
630        self.default_sheet_id = id;
631    }
632
633    /// Returns the ID for a sheet name, creating one if it doesn't exist.
634    pub fn sheet_id_mut(&mut self, name: &str) -> SheetId {
635        self.sheet_reg.id_for(name)
636    }
637
638    pub fn sheet_id(&self, name: &str) -> Option<SheetId> {
639        self.sheet_reg.get_id(name)
640    }
641
642    /// Resolve a sheet name to an existing ID or return a #REF! error.
643    fn resolve_existing_sheet_id(&self, name: &str) -> Result<SheetId, ExcelError> {
644        self.sheet_id(name).ok_or_else(|| {
645            ExcelError::new(ExcelErrorKind::Ref).with_message(format!("Sheet not found: {name}"))
646        })
647    }
648
649    /// Returns the name of a sheet given its ID.
650    pub fn sheet_name(&self, id: SheetId) -> &str {
651        self.sheet_reg.name(id)
652    }
653
654    /// Access the sheet registry (read-only) for external bindings
655    pub fn sheet_reg(&self) -> &SheetRegistry {
656        &self.sheet_reg
657    }
658
659    /// Converts a `CellRef` to a fully qualified A1-style string (e.g., "Sheet1!A1").
660    pub fn to_a1(&self, cell_ref: CellRef) -> String {
661        format!("{}!{}", self.sheet_name(cell_ref.sheet_id), cell_ref.coord)
662    }
663
664    // Named Range Methods
665
666    /// Define a new named range
667    pub fn define_name(
668        &mut self,
669        name: &str,
670        definition: NamedDefinition,
671        scope: NameScope,
672    ) -> Result<(), ExcelError> {
673        // Validate name
674        if !is_valid_excel_name(name) {
675            return Err(
676                ExcelError::new(ExcelErrorKind::Name).with_message(format!("Invalid name: {name}"))
677            );
678        }
679
680        // Check for duplicates
681        match scope {
682            NameScope::Workbook => {
683                if self.named_ranges.contains_key(name) {
684                    return Err(ExcelError::new(ExcelErrorKind::Name)
685                        .with_message(format!("Name already exists: {name}")));
686                }
687            }
688            NameScope::Sheet(sheet_id) => {
689                if self
690                    .sheet_named_ranges
691                    .contains_key(&(sheet_id, name.to_string()))
692                {
693                    return Err(ExcelError::new(ExcelErrorKind::Name)
694                        .with_message(format!("Name already exists in sheet: {name}")));
695                }
696            }
697        }
698
699        // Extract dependencies if formula
700        let named_range = match definition {
701            NamedDefinition::Formula { ref ast, .. } => {
702                let (deps, range_deps, _) = self.extract_dependencies(
703                    ast,
704                    match scope {
705                        NameScope::Sheet(id) => id,
706                        NameScope::Workbook => self.default_sheet_id,
707                    },
708                )?;
709                NamedRange {
710                    definition: NamedDefinition::Formula {
711                        ast: ast.clone(),
712                        dependencies: deps,
713                        range_deps,
714                    },
715                    scope,
716                    dependents: FxHashSet::default(),
717                }
718            }
719            _ => NamedRange {
720                definition,
721                scope,
722                dependents: FxHashSet::default(),
723            },
724        };
725
726        // Store
727        match scope {
728            NameScope::Workbook => {
729                self.named_ranges.insert(name.to_string(), named_range);
730            }
731            NameScope::Sheet(id) => {
732                self.sheet_named_ranges
733                    .insert((id, name.to_string()), named_range);
734            }
735        }
736
737        Ok(())
738    }
739
740    /// Iterate workbook-scoped named ranges (for bindings/testing)
741    pub fn named_ranges_iter(&self) -> impl Iterator<Item = (&String, &NamedRange)> {
742        self.named_ranges.iter()
743    }
744
745    /// Iterate sheet-scoped named ranges (for bindings/testing)
746    pub fn sheet_named_ranges_iter(
747        &self,
748    ) -> impl Iterator<Item = (&(SheetId, String), &NamedRange)> {
749        self.sheet_named_ranges.iter()
750    }
751
752    /// Resolve a named range to its definition
753    pub fn resolve_name(&self, name: &str, current_sheet: SheetId) -> Option<&NamedDefinition> {
754        // Sheet scope takes precedence
755        self.sheet_named_ranges
756            .get(&(current_sheet, name.to_string()))
757            .or_else(|| self.named_ranges.get(name))
758            .map(|nr| &nr.definition)
759    }
760
761    /// Update an existing named range definition
762    pub fn update_name(
763        &mut self,
764        name: &str,
765        new_definition: NamedDefinition,
766        scope: NameScope,
767    ) -> Result<(), ExcelError> {
768        // First collect dependents to avoid borrow checker issues
769        let dependents_to_dirty = match scope {
770            NameScope::Workbook => self
771                .named_ranges
772                .get(name)
773                .map(|nr| nr.dependents.iter().copied().collect::<Vec<_>>()),
774            NameScope::Sheet(id) => self
775                .sheet_named_ranges
776                .get(&(id, name.to_string()))
777                .map(|nr| nr.dependents.iter().copied().collect::<Vec<_>>()),
778        };
779
780        if let Some(dependents) = dependents_to_dirty {
781            // Mark all dependents as dirty
782            for vertex_id in dependents {
783                self.mark_vertex_dirty(vertex_id);
784            }
785
786            // Now update the definition
787            let named_range = match scope {
788                NameScope::Workbook => self.named_ranges.get_mut(name),
789                NameScope::Sheet(id) => self.sheet_named_ranges.get_mut(&(id, name.to_string())),
790            };
791
792            if let Some(named_range) = named_range {
793                // Update definition
794                named_range.definition = new_definition;
795
796                // Clear dependents (will be rebuilt on next evaluation)
797                named_range.dependents.clear();
798            }
799
800            Ok(())
801        } else {
802            Err(ExcelError::new(ExcelErrorKind::Name)
803                .with_message(format!("Name not found: {name}")))
804        }
805    }
806
807    /// Delete a named range
808    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
809        let named_range = match scope {
810            NameScope::Workbook => self.named_ranges.remove(name),
811            NameScope::Sheet(id) => self.sheet_named_ranges.remove(&(id, name.to_string())),
812        };
813
814        if let Some(named_range) = named_range {
815            // Mark all dependent formulas as needing recalculation with #NAME! error
816            for &vertex_id in &named_range.dependents {
817                // Mark as dirty to trigger recalculation (will error during eval)
818                self.mark_vertex_dirty(vertex_id);
819                // Remove from vertex_to_names mapping
820                if let Some(names) = self.vertex_to_names.get_mut(&vertex_id) {
821                    names.retain(|n| n != name);
822                }
823            }
824            Ok(())
825        } else {
826            Err(ExcelError::new(ExcelErrorKind::Name)
827                .with_message(format!("Name not found: {name}")))
828        }
829    }
830
831    #[cfg(test)]
832    pub(crate) fn formula_to_range_deps(&self) -> &FxHashMap<VertexId, Vec<ReferenceType>> {
833        &self.formula_to_range_deps
834    }
835
836    #[cfg(test)]
837    pub(crate) fn stripe_to_dependents(&self) -> &FxHashMap<StripeKey, FxHashSet<VertexId>> {
838        &self.stripe_to_dependents
839    }
840
841    pub(crate) fn vertex_len(&self) -> usize {
842        self.store.len()
843    }
844
845    /// Return the compressed range dependencies recorded for a formula vertex, if any.
846    /// These are `ReferenceType::Range` entries that were not expanded into explicit
847    /// cell edges due to `range_expansion_limit` or due to infinite/partial bounds.
848    pub fn get_range_dependencies(
849        &self,
850        vertex: VertexId,
851    ) -> Option<&Vec<formualizer_parse::parser::ReferenceType>> {
852        self.formula_to_range_deps.get(&vertex)
853    }
854
855    /// Get mutable access to a sheet's index, creating it if it doesn't exist
856    /// This is the primary way VertexEditor and internal operations access the index
857    pub fn sheet_index_mut(&mut self, sheet_id: SheetId) -> &mut SheetIndex {
858        self.sheet_indexes.entry(sheet_id).or_default()
859    }
860
861    /// Get immutable access to a sheet's index, returns None if not initialized
862    pub fn sheet_index(&self, sheet_id: SheetId) -> Option<&SheetIndex> {
863        self.sheet_indexes.get(&sheet_id)
864    }
865
866    /// Set a value in a cell, returns affected vertex IDs
867    pub fn set_cell_value(
868        &mut self,
869        sheet: &str,
870        row: u32,
871        col: u32,
872        value: LiteralValue,
873    ) -> Result<OperationSummary, ExcelError> {
874        let sheet_id = self.sheet_id_mut(sheet);
875        let coord = Coord::new(row, col, true, true); // Assuming absolute reference for direct sets
876        let addr = CellRef::new(sheet_id, coord);
877        let mut created_placeholders = Vec::new();
878
879        let vertex_id = if let Some(&existing_id) = self.cell_to_vertex.get(&addr) {
880            // Check if it was a formula and remove dependencies
881            let is_formula = matches!(
882                self.store.kind(existing_id),
883                VertexKind::FormulaScalar | VertexKind::FormulaArray
884            );
885
886            if is_formula {
887                self.remove_dependent_edges(existing_id);
888                self.vertex_formulas.remove(&existing_id);
889            }
890
891            // Update to value kind
892            self.store.set_kind(existing_id, VertexKind::Cell);
893            let value_ref = self.data_store.store_value(value);
894            self.vertex_values.insert(existing_id, value_ref);
895            existing_id
896        } else {
897            // Create new vertex
898            created_placeholders.push(addr);
899            let packed_coord = PackedCoord::new(row, col);
900            let vertex_id = self.store.allocate(packed_coord, sheet_id, 0x01); // dirty flag
901
902            // Add vertex coordinate for CSR
903            self.edges.add_vertex(packed_coord, vertex_id.0);
904
905            // Add to sheet index for O(log n + k) range queries
906            self.sheet_index_mut(sheet_id)
907                .add_vertex(packed_coord, vertex_id);
908
909            self.store.set_kind(vertex_id, VertexKind::Cell);
910            let value_ref = self.data_store.store_value(value);
911            self.vertex_values.insert(vertex_id, value_ref);
912            self.cell_to_vertex.insert(addr, vertex_id);
913            vertex_id
914        };
915
916        Ok(OperationSummary {
917            affected_vertices: self.mark_dirty(vertex_id),
918            created_placeholders,
919        })
920    }
921
922    /// Reserve capacity hints for upcoming bulk cell inserts (values only for now).
923    pub fn reserve_cells(&mut self, additional: usize) {
924        self.store.reserve(additional);
925        self.vertex_values.reserve(additional);
926        self.cell_to_vertex.reserve(additional);
927        // sheet_indexes: cannot easily reserve per-sheet without distribution; skip.
928    }
929
930    /// Fast path for initial bulk load of value cells: avoids dirty propagation & dependency work.
931    pub fn set_cell_value_bulk_untracked(
932        &mut self,
933        sheet: &str,
934        row: u32,
935        col: u32,
936        value: LiteralValue,
937    ) {
938        let sheet_id = self.sheet_id_mut(sheet);
939        let coord = Coord::new(row, col, true, true);
940        let addr = CellRef::new(sheet_id, coord);
941        if let Some(&existing_id) = self.cell_to_vertex.get(&addr) {
942            // Overwrite existing value vertex only (ignore formulas in bulk path)
943            let value_ref = self.data_store.store_value(value);
944            self.vertex_values.insert(existing_id, value_ref);
945            self.store.set_kind(existing_id, VertexKind::Cell);
946            return;
947        }
948        let packed_coord = PackedCoord::new(row, col);
949        let vertex_id = self.store.allocate(packed_coord, sheet_id, 0x00); // not dirty
950        self.edges.add_vertex(packed_coord, vertex_id.0);
951        self.sheet_index_mut(sheet_id)
952            .add_vertex(packed_coord, vertex_id);
953        self.store.set_kind(vertex_id, VertexKind::Cell);
954        let value_ref = self.data_store.store_value(value);
955        self.vertex_values.insert(vertex_id, value_ref);
956        self.cell_to_vertex.insert(addr, vertex_id);
957    }
958
959    /// Bulk insert a collection of plain value cells (no formulas) more efficiently.
960    pub fn bulk_insert_values<I>(&mut self, sheet: &str, cells: I)
961    where
962        I: IntoIterator<Item = (u32, u32, LiteralValue)>,
963    {
964        use std::time::Instant;
965        let t0 = Instant::now();
966        // Collect first to know size
967        let collected: Vec<(u32, u32, LiteralValue)> = cells.into_iter().collect();
968        if collected.is_empty() {
969            return;
970        }
971        let sheet_id = self.sheet_id_mut(sheet);
972        self.reserve_cells(collected.len());
973        let t_reserve = Instant::now();
974        let mut new_vertices: Vec<(PackedCoord, u32)> = Vec::with_capacity(collected.len());
975        let mut index_items: Vec<(PackedCoord, VertexId)> = Vec::with_capacity(collected.len());
976        // For new allocations, accumulate values and assign after a single batch store
977        let mut new_value_coords: Vec<(PackedCoord, VertexId)> =
978            Vec::with_capacity(collected.len());
979        let mut new_value_literals: Vec<LiteralValue> = Vec::with_capacity(collected.len());
980        // Detect fast path: during initial ingest, caller may guarantee most cells are new.
981        let assume_new = self.first_load_assume_new
982            && self
983                .sheet_id(sheet)
984                .map(|sid| !self.ensure_touched_sheets.contains(&sid))
985                .unwrap_or(false);
986
987        for (row, col, value) in collected {
988            let coord = Coord::new(row, col, true, true);
989            let addr = CellRef::new(sheet_id, coord);
990            if !assume_new {
991                if let Some(&existing_id) = self.cell_to_vertex.get(&addr) {
992                    let value_ref = self.data_store.store_value(value);
993                    self.vertex_values.insert(existing_id, value_ref);
994                    self.store.set_kind(existing_id, VertexKind::Cell);
995                    continue;
996                }
997            }
998            let packed = PackedCoord::new(row, col);
999            let vertex_id = self.store.allocate(packed, sheet_id, 0x00);
1000            self.store.set_kind(vertex_id, VertexKind::Cell);
1001            // Defer value arena storage to a single batch
1002            new_value_coords.push((packed, vertex_id));
1003            new_value_literals.push(value);
1004            self.cell_to_vertex.insert(addr, vertex_id);
1005            new_vertices.push((packed, vertex_id.0));
1006            index_items.push((packed, vertex_id));
1007        }
1008        // Perform a single batch store for newly allocated values
1009        if !new_value_literals.is_empty() {
1010            let vrefs = self.data_store.store_values_batch(new_value_literals);
1011            debug_assert_eq!(vrefs.len(), new_value_coords.len());
1012            for (i, (_pc, vid)) in new_value_coords.iter().enumerate() {
1013                self.vertex_values.insert(*vid, vrefs[i]);
1014            }
1015        }
1016        let t_after_alloc = Instant::now();
1017        if !new_vertices.is_empty() {
1018            let t_edges_start = Instant::now();
1019            self.edges.add_vertices_batch(&new_vertices);
1020            let t_edges_done = Instant::now();
1021
1022            match self.config.sheet_index_mode {
1023                crate::engine::SheetIndexMode::Eager => {
1024                    self.sheet_index_mut(sheet_id)
1025                        .add_vertices_batch(&index_items);
1026                }
1027                crate::engine::SheetIndexMode::Lazy => {
1028                    // Skip building index now; will be built on-demand
1029                }
1030                crate::engine::SheetIndexMode::FastBatch => {
1031                    // FastBatch for now delegates to same batch insert (future: build from sorted arrays)
1032                    self.sheet_index_mut(sheet_id)
1033                        .add_vertices_batch(&index_items);
1034                }
1035            }
1036            let t_index_done = Instant::now();
1037        }
1038    }
1039
1040    /// Set a formula in a cell, returns affected vertex IDs
1041    pub fn set_cell_formula(
1042        &mut self,
1043        sheet: &str,
1044        row: u32,
1045        col: u32,
1046        ast: ASTNode,
1047    ) -> Result<OperationSummary, ExcelError> {
1048        let volatile = self.is_ast_volatile(&ast);
1049        self.set_cell_formula_with_volatility(sheet, row, col, ast, volatile)
1050    }
1051
1052    /// Set a formula in a cell with a known volatility flag (context-scoped detection upstream)
1053    pub fn set_cell_formula_with_volatility(
1054        &mut self,
1055        sheet: &str,
1056        row: u32,
1057        col: u32,
1058        ast: ASTNode,
1059        volatile: bool,
1060    ) -> Result<OperationSummary, ExcelError> {
1061        let dbg = std::env::var("FZ_DEBUG_LOAD")
1062            .ok()
1063            .is_some_and(|v| v != "0");
1064        let dep_ms_thresh: u128 = std::env::var("FZ_DEBUG_DEP_MS")
1065            .ok()
1066            .and_then(|s| s.parse().ok())
1067            .unwrap_or(0);
1068        let sample_n: usize = std::env::var("FZ_DEBUG_SAMPLE_N")
1069            .ok()
1070            .and_then(|s| s.parse().ok())
1071            .unwrap_or(0);
1072        let t0 = if dbg {
1073            Some(std::time::Instant::now())
1074        } else {
1075            None
1076        };
1077        let sheet_id = self.sheet_id_mut(sheet);
1078        let coord = Coord::new(row, col, true, true);
1079        let addr = CellRef::new(sheet_id, coord);
1080
1081        // Extract dependencies from AST, creating placeholders if needed
1082        let t_dep0 = if dbg {
1083            Some(std::time::Instant::now())
1084        } else {
1085            None
1086        };
1087        let (new_dependencies, new_range_dependencies, mut created_placeholders) =
1088            self.extract_dependencies(&ast, sheet_id)?;
1089        if let (true, Some(t)) = (dbg, t_dep0) {
1090            let elapsed = t.elapsed().as_millis();
1091            // Only log if over threshold or sampled
1092            let do_log = (dep_ms_thresh > 0 && elapsed >= dep_ms_thresh)
1093                || (sample_n > 0 && (row as usize % sample_n == 0));
1094            if dep_ms_thresh == 0 && sample_n == 0 {
1095                // default: very light sampling every 1000 rows
1096                if (row % 1000) == 0 {
1097                    eprintln!(
1098                        "[fz][dep] {}!{} extracted: deps={}, ranges={}, placeholders={} in {} ms",
1099                        self.sheet_name(sheet_id),
1100                        crate::reference::Coord::new(row, col, true, true),
1101                        new_dependencies.len(),
1102                        new_range_dependencies.len(),
1103                        created_placeholders.len(),
1104                        elapsed
1105                    );
1106                }
1107            } else if do_log {
1108                eprintln!(
1109                    "[fz][dep] {}!{} extracted: deps={}, ranges={}, placeholders={} in {} ms",
1110                    self.sheet_name(sheet_id),
1111                    crate::reference::Coord::new(row, col, true, true),
1112                    new_dependencies.len(),
1113                    new_range_dependencies.len(),
1114                    created_placeholders.len(),
1115                    elapsed
1116                );
1117            }
1118        }
1119
1120        // Check for self-reference (immediate cycle detection)
1121        let addr_vertex_id = self.get_or_create_vertex(&addr, &mut created_placeholders);
1122
1123        if new_dependencies.contains(&addr_vertex_id) {
1124            return Err(ExcelError::new(ExcelErrorKind::Circ)
1125                .with_message("Self-reference detected".to_string()));
1126        }
1127
1128        // Remove old dependencies first
1129        self.remove_dependent_edges(addr_vertex_id);
1130
1131        // Update vertex properties
1132        self.store
1133            .set_kind(addr_vertex_id, VertexKind::FormulaScalar);
1134        let ast_id = self.data_store.store_ast(&ast, &self.sheet_reg);
1135        self.vertex_formulas.insert(addr_vertex_id, ast_id);
1136        self.store.set_dirty(addr_vertex_id, true);
1137
1138        // Clear any cached value since this is now a formula
1139        self.vertex_values.remove(&addr_vertex_id);
1140
1141        if volatile {
1142            self.store.set_volatile(addr_vertex_id, true);
1143        }
1144
1145        if let (true, Some(t)) = (dbg, t0) {
1146            let elapsed = t.elapsed().as_millis();
1147            let log_set = dep_ms_thresh > 0 && elapsed >= dep_ms_thresh;
1148            if log_set {
1149                eprintln!(
1150                    "[fz][set] {}!{} total {} ms",
1151                    self.sheet_name(sheet_id),
1152                    crate::reference::Coord::new(row, col, true, true),
1153                    elapsed
1154                );
1155            }
1156        }
1157
1158        // Add new dependency edges
1159        self.add_dependent_edges(addr_vertex_id, &new_dependencies);
1160        self.add_range_dependent_edges(addr_vertex_id, &new_range_dependencies, sheet_id);
1161
1162        // Mark as volatile if needed
1163        if volatile {
1164            self.volatile_vertices.insert(addr_vertex_id);
1165        }
1166
1167        Ok(OperationSummary {
1168            affected_vertices: self.mark_dirty(addr_vertex_id),
1169            created_placeholders,
1170        })
1171    }
1172
1173    /// Get current value from a cell
1174    pub fn get_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
1175        let sheet_id = self.sheet_reg.get_id(sheet)?;
1176        let coord = Coord::new(row, col, true, true);
1177        let addr = CellRef::new(sheet_id, coord);
1178
1179        self.cell_to_vertex.get(&addr).and_then(|&vertex_id| {
1180            // Check values hashmap (stores both cell values and formula results)
1181            self.vertex_values
1182                .get(&vertex_id)
1183                .map(|&value_ref| self.data_store.retrieve_value(value_ref))
1184        })
1185    }
1186
1187    /// Mark vertex dirty and propagate to dependents
1188    fn mark_dirty(&mut self, vertex_id: VertexId) -> Vec<VertexId> {
1189        let mut affected = FxHashSet::default();
1190        let mut to_visit = Vec::new();
1191        let mut visited_for_propagation = FxHashSet::default();
1192
1193        // Only mark the source vertex as dirty if it's a formula
1194        // Value cells don't get marked dirty themselves but are still affected
1195        let is_formula = matches!(
1196            self.store.kind(vertex_id),
1197            VertexKind::FormulaScalar | VertexKind::FormulaArray
1198        );
1199
1200        if is_formula {
1201            to_visit.push(vertex_id);
1202        } else {
1203            // Value cells are affected (for tracking) but not marked dirty
1204            affected.insert(vertex_id);
1205        }
1206
1207        // Initial propagation from direct and range dependents
1208        {
1209            // Get dependents (vertices that depend on this vertex)
1210            let dependents = self.get_dependents(vertex_id);
1211            to_visit.extend(&dependents);
1212
1213            // Check range dependencies
1214            let view = self.store.view(vertex_id);
1215            let row = view.row();
1216            let col = view.col();
1217            let dirty_sheet_id = view.sheet_id();
1218
1219            // New stripe-based dependents lookup
1220            let mut potential_dependents = FxHashSet::default();
1221
1222            // 1. Column stripe lookup
1223            let column_key = StripeKey {
1224                sheet_id: dirty_sheet_id,
1225                stripe_type: StripeType::Column,
1226                index: col,
1227            };
1228            if let Some(dependents) = self.stripe_to_dependents.get(&column_key) {
1229                potential_dependents.extend(dependents);
1230            }
1231
1232            // 2. Row stripe lookup
1233            let row_key = StripeKey {
1234                sheet_id: dirty_sheet_id,
1235                stripe_type: StripeType::Row,
1236                index: row,
1237            };
1238            if let Some(dependents) = self.stripe_to_dependents.get(&row_key) {
1239                potential_dependents.extend(dependents);
1240            }
1241
1242            // 3. Block stripe lookup
1243            if self.config.enable_block_stripes {
1244                let block_key = StripeKey {
1245                    sheet_id: dirty_sheet_id,
1246                    stripe_type: StripeType::Block,
1247                    index: block_index(row, col),
1248                };
1249                if let Some(dependents) = self.stripe_to_dependents.get(&block_key) {
1250                    potential_dependents.extend(dependents);
1251                }
1252            }
1253
1254            // Precision check: ensure the dirtied cell is actually within the formula's range
1255            for &dep_id in &potential_dependents {
1256                if let Some(ranges) = self.formula_to_range_deps.get(&dep_id) {
1257                    for range in ranges {
1258                        if let ReferenceType::Range {
1259                            sheet,
1260                            start_row,
1261                            start_col,
1262                            end_row,
1263                            end_col,
1264                        } = range
1265                        {
1266                            let range_sheet_name = sheet
1267                                .as_deref()
1268                                .unwrap_or_else(|| self.sheet_name(dirty_sheet_id));
1269                            if let Some(range_sheet_id) = self.sheet_reg.get_id(range_sheet_name) {
1270                                if range_sheet_id == dirty_sheet_id
1271                                    && row >= start_row.unwrap_or(1)
1272                                    && row <= end_row.unwrap_or(u32::MAX)
1273                                    && col >= start_col.unwrap_or(1)
1274                                    && col <= end_col.unwrap_or(u32::MAX)
1275                                {
1276                                    to_visit.push(dep_id);
1277                                    break; // Found a matching range
1278                                }
1279                            }
1280                        }
1281                    }
1282                }
1283            }
1284        }
1285
1286        while let Some(id) = to_visit.pop() {
1287            if !visited_for_propagation.insert(id) {
1288                continue; // Already processed
1289            }
1290            affected.insert(id);
1291
1292            // Mark vertex as dirty
1293            self.store.set_dirty(id, true);
1294
1295            // Add direct dependents to visit list
1296            let dependents = self.get_dependents(id);
1297            to_visit.extend(&dependents);
1298        }
1299
1300        // Add to dirty set
1301        self.dirty_vertices.extend(&affected);
1302
1303        // Return as Vec for compatibility
1304        affected.into_iter().collect()
1305    }
1306
1307    /// Get all vertices that need evaluation
1308    pub fn get_evaluation_vertices(&self) -> Vec<VertexId> {
1309        let mut combined = FxHashSet::default();
1310        combined.extend(&self.dirty_vertices);
1311        combined.extend(&self.volatile_vertices);
1312
1313        let mut result: Vec<VertexId> = combined
1314            .into_iter()
1315            .filter(|&id| {
1316                // Only include formula vertices
1317                matches!(
1318                    self.store.kind(id),
1319                    VertexKind::FormulaScalar | VertexKind::FormulaArray
1320                )
1321            })
1322            .collect();
1323        result.sort_unstable();
1324        result
1325    }
1326
1327    /// Clear dirty flags after successful evaluation
1328    pub fn clear_dirty_flags(&mut self, vertices: &[VertexId]) {
1329        for &vertex_id in vertices {
1330            self.store.set_dirty(vertex_id, false);
1331            self.dirty_vertices.remove(&vertex_id);
1332        }
1333    }
1334
1335    /// đź”® Scalability Hook: Clear volatile vertices after evaluation cycle
1336    pub fn clear_volatile_flags(&mut self) {
1337        self.volatile_vertices.clear();
1338    }
1339
1340    /// Re-marks all volatile vertices as dirty for the next evaluation cycle.
1341    pub(crate) fn redirty_volatiles(&mut self) {
1342        let volatile_ids: Vec<VertexId> = self.volatile_vertices.iter().copied().collect();
1343        for id in volatile_ids {
1344            self.mark_dirty(id);
1345        }
1346    }
1347
1348    // Helper methods
1349    fn extract_dependencies(
1350        &mut self,
1351        ast: &ASTNode,
1352        current_sheet_id: SheetId,
1353    ) -> ExtractDependenciesResult {
1354        let mut dependencies = FxHashSet::default();
1355        let mut range_dependencies = Vec::new();
1356        let mut created_placeholders = Vec::new();
1357        self.extract_dependencies_recursive(
1358            ast,
1359            current_sheet_id,
1360            &mut dependencies,
1361            &mut range_dependencies,
1362            &mut created_placeholders,
1363        )?;
1364
1365        // Deduplicate range references
1366        let mut deduped_ranges = Vec::new();
1367        for range_ref in range_dependencies {
1368            if !deduped_ranges.contains(&range_ref) {
1369                deduped_ranges.push(range_ref);
1370            }
1371        }
1372
1373        Ok((
1374            dependencies.into_iter().collect(),
1375            deduped_ranges,
1376            created_placeholders,
1377        ))
1378    }
1379
1380    fn extract_dependencies_recursive(
1381        &mut self,
1382        ast: &ASTNode,
1383        current_sheet_id: SheetId,
1384        dependencies: &mut FxHashSet<VertexId>,
1385        range_dependencies: &mut Vec<ReferenceType>,
1386        created_placeholders: &mut Vec<CellRef>,
1387    ) -> Result<(), ExcelError> {
1388        match &ast.node_type {
1389            ASTNodeType::Reference { reference, .. } => {
1390                match reference {
1391                    ReferenceType::Cell { .. } => {
1392                        let vertex_id = self.get_or_create_vertex_for_reference(
1393                            reference,
1394                            current_sheet_id,
1395                            created_placeholders,
1396                        )?;
1397                        dependencies.insert(vertex_id);
1398                    }
1399                    ReferenceType::Range {
1400                        sheet,
1401                        start_row,
1402                        start_col,
1403                        end_row,
1404                        end_col,
1405                    } => {
1406                        // If any bound is missing (infinite/partial range), always keep compressed
1407                        let has_unbounded = start_row.is_none()
1408                            || end_row.is_none()
1409                            || start_col.is_none()
1410                            || end_col.is_none();
1411                        if has_unbounded {
1412                            range_dependencies.push(reference.clone());
1413                        } else {
1414                            let sr = start_row.unwrap();
1415                            let sc = start_col.unwrap();
1416                            let er = end_row.unwrap();
1417                            let ec = end_col.unwrap();
1418
1419                            if sr > er || sc > ec {
1420                                return Err(ExcelError::new(ExcelErrorKind::Ref));
1421                            }
1422
1423                            let height = er.saturating_sub(sr) + 1;
1424                            let width = ec.saturating_sub(sc) + 1;
1425                            let size = (width * height) as usize;
1426
1427                            if size <= self.config.range_expansion_limit {
1428                                // Expand to individual cells
1429                                let sheet_id = match sheet {
1430                                    Some(name) => self.resolve_existing_sheet_id(name)?,
1431                                    None => current_sheet_id,
1432                                };
1433                                for row in sr..=er {
1434                                    for col in sc..=ec {
1435                                        let coord = Coord::new(row, col, true, true);
1436                                        let addr = CellRef::new(sheet_id, coord);
1437                                        let vertex_id =
1438                                            self.get_or_create_vertex(&addr, created_placeholders);
1439                                        dependencies.insert(vertex_id);
1440                                    }
1441                                }
1442                            } else {
1443                                // Keep as a compressed range dependency
1444                                range_dependencies.push(reference.clone());
1445                            }
1446                        }
1447                    }
1448                    ReferenceType::NamedRange(name) => {
1449                        // Resolve the named range
1450                        if let Some(definition) = self.resolve_name(name, current_sheet_id) {
1451                            // Clone the definition to avoid borrow issues
1452                            let definition = definition.clone();
1453
1454                            match definition {
1455                                NamedDefinition::Cell(cell_ref) => {
1456                                    let vertex_id =
1457                                        self.get_or_create_vertex(&cell_ref, created_placeholders);
1458                                    dependencies.insert(vertex_id);
1459                                }
1460                                NamedDefinition::Range(range_ref) => {
1461                                    // Calculate range size
1462                                    let height = range_ref
1463                                        .end
1464                                        .coord
1465                                        .row
1466                                        .saturating_sub(range_ref.start.coord.row)
1467                                        + 1;
1468                                    let width = range_ref
1469                                        .end
1470                                        .coord
1471                                        .col
1472                                        .saturating_sub(range_ref.start.coord.col)
1473                                        + 1;
1474                                    let size = (width * height) as usize;
1475
1476                                    if size <= self.config.range_expansion_limit {
1477                                        // Expand to individual cells
1478                                        for row in
1479                                            range_ref.start.coord.row..=range_ref.end.coord.row
1480                                        {
1481                                            for col in
1482                                                range_ref.start.coord.col..=range_ref.end.coord.col
1483                                            {
1484                                                let coord = Coord::new(row, col, true, true);
1485                                                let addr =
1486                                                    CellRef::new(range_ref.start.sheet_id, coord);
1487                                                let vertex_id = self.get_or_create_vertex(
1488                                                    &addr,
1489                                                    created_placeholders,
1490                                                );
1491                                                dependencies.insert(vertex_id);
1492                                            }
1493                                        }
1494                                    } else {
1495                                        // Keep as compressed range
1496                                        let sheet_name = self.sheet_name(range_ref.start.sheet_id);
1497                                        range_dependencies.push(ReferenceType::Range {
1498                                            sheet: Some(sheet_name.to_string()),
1499                                            start_row: Some(range_ref.start.coord.row),
1500                                            start_col: Some(range_ref.start.coord.col),
1501                                            end_row: Some(range_ref.end.coord.row),
1502                                            end_col: Some(range_ref.end.coord.col),
1503                                        });
1504                                    }
1505                                }
1506                                NamedDefinition::Formula {
1507                                    dependencies: formula_deps,
1508                                    range_deps,
1509                                    ..
1510                                } => {
1511                                    // Add pre-computed dependencies
1512                                    dependencies.extend(formula_deps);
1513                                    range_dependencies.extend(range_deps);
1514                                }
1515                            }
1516
1517                            // Note: We should track that this vertex uses this name for invalidation
1518                            // This will be done after the vertex is created in set_cell_formula
1519                        } else {
1520                            return Err(ExcelError::new(ExcelErrorKind::Name)
1521                                .with_message(format!("Undefined name: {name}")));
1522                        }
1523                    }
1524                    _ => {} // Ignore others for now
1525                }
1526            }
1527            ASTNodeType::BinaryOp { left, right, .. } => {
1528                self.extract_dependencies_recursive(
1529                    left,
1530                    current_sheet_id,
1531                    dependencies,
1532                    range_dependencies,
1533                    created_placeholders,
1534                )?;
1535                self.extract_dependencies_recursive(
1536                    right,
1537                    current_sheet_id,
1538                    dependencies,
1539                    range_dependencies,
1540                    created_placeholders,
1541                )?;
1542            }
1543            ASTNodeType::UnaryOp { expr, .. } => {
1544                self.extract_dependencies_recursive(
1545                    expr,
1546                    current_sheet_id,
1547                    dependencies,
1548                    range_dependencies,
1549                    created_placeholders,
1550                )?;
1551            }
1552            ASTNodeType::Function { args, .. } => {
1553                for arg in args {
1554                    self.extract_dependencies_recursive(
1555                        arg,
1556                        current_sheet_id,
1557                        dependencies,
1558                        range_dependencies,
1559                        created_placeholders,
1560                    )?;
1561                }
1562            }
1563            ASTNodeType::Array(rows) => {
1564                for row in rows {
1565                    for cell in row {
1566                        self.extract_dependencies_recursive(
1567                            cell,
1568                            current_sheet_id,
1569                            dependencies,
1570                            range_dependencies,
1571                            created_placeholders,
1572                        )?;
1573                    }
1574                }
1575            }
1576            ASTNodeType::Literal(_) => {
1577                // Literals have no dependencies
1578            }
1579        }
1580        Ok(())
1581    }
1582
1583    fn get_or_create_vertex(
1584        &mut self,
1585        addr: &CellRef,
1586        created_placeholders: &mut Vec<CellRef>,
1587    ) -> VertexId {
1588        if let Some(&vertex_id) = self.cell_to_vertex.get(addr) {
1589            return vertex_id;
1590        }
1591
1592        created_placeholders.push(*addr);
1593        let packed_coord = PackedCoord::new(addr.coord.row, addr.coord.col);
1594        let vertex_id = self.store.allocate(packed_coord, addr.sheet_id, 0x00);
1595
1596        // Add vertex coordinate for CSR
1597        self.edges.add_vertex(packed_coord, vertex_id.0);
1598
1599        // Add to sheet index for O(log n + k) range queries
1600        self.sheet_index_mut(addr.sheet_id)
1601            .add_vertex(packed_coord, vertex_id);
1602
1603        self.store.set_kind(vertex_id, VertexKind::Empty);
1604        self.cell_to_vertex.insert(*addr, vertex_id);
1605        vertex_id
1606    }
1607
1608    /// Gets the VertexId for a reference, creating a placeholder vertex if it doesn't exist.
1609    fn get_or_create_vertex_for_reference(
1610        &mut self,
1611        reference: &ReferenceType,
1612        current_sheet_id: SheetId,
1613        created_placeholders: &mut Vec<CellRef>,
1614    ) -> Result<VertexId, ExcelError> {
1615        match reference {
1616            ReferenceType::Cell { sheet, row, col } => {
1617                let sheet_id = match sheet {
1618                    Some(name) => self.resolve_existing_sheet_id(name)?,
1619                    None => current_sheet_id,
1620                };
1621                let coord = Coord::new(*row, *col, true, true);
1622                let addr = CellRef::new(sheet_id, coord);
1623                Ok(self.get_or_create_vertex(&addr, created_placeholders))
1624            }
1625            _ => Err(ExcelError::new(ExcelErrorKind::Value)
1626                .with_message("Expected a cell reference, but got a range or other type.")),
1627        }
1628    }
1629
1630    #[inline]
1631    fn is_ast_volatile(&self, ast: &ASTNode) -> bool {
1632        // Fast-path if the AST already carries volatility info from the parser.
1633        ast.contains_volatile()
1634    }
1635
1636    fn add_dependent_edges(&mut self, dependent: VertexId, dependencies: &[VertexId]) {
1637        // Batch to avoid repeated CSR rebuilds and keep reverse edges current
1638        self.edges.begin_batch();
1639
1640        // If PK enabled, update order using a short-lived adapter without holding &mut self
1641        // Track dependencies that should be skipped if rejecting cycle-creating edges
1642        let mut skip_deps: rustc_hash::FxHashSet<VertexId> = rustc_hash::FxHashSet::default();
1643        if self.pk_order.is_some() {
1644            if let Some(mut pk) = self.pk_order.take() {
1645                pk.ensure_nodes(std::iter::once(dependent));
1646                pk.ensure_nodes(dependencies.iter().copied());
1647                {
1648                    let adapter = GraphAdapter { g: self };
1649                    for &dep_id in dependencies {
1650                        match pk.try_add_edge(&adapter, dep_id, dependent) {
1651                            Ok(_) => {}
1652                            Err(_cycle) => {
1653                                if self.config.pk_reject_cycle_edges {
1654                                    skip_deps.insert(dep_id);
1655                                } else {
1656                                    pk.rebuild_full(&adapter);
1657                                }
1658                            }
1659                        }
1660                    }
1661                } // drop adapter
1662                self.pk_order = Some(pk);
1663            }
1664        }
1665
1666        // Now mutate engine edges; if rejecting cycles, re-check and skip those that would create cycles
1667        for &dep_id in dependencies {
1668            if self.config.pk_reject_cycle_edges && skip_deps.contains(&dep_id) {
1669                continue;
1670            }
1671            self.edges.add_edge(dependent, dep_id);
1672            #[cfg(test)]
1673            {
1674                self.instr.edges_added += 1;
1675            }
1676        }
1677
1678        self.edges.end_batch();
1679    }
1680
1681    /// Like add_dependent_edges, but assumes caller is managing edges.begin_batch/end_batch
1682    fn add_dependent_edges_nobatch(&mut self, dependent: VertexId, dependencies: &[VertexId]) {
1683        // If PK enabled, update order using a short-lived adapter without holding &mut self
1684        let mut skip_deps: rustc_hash::FxHashSet<VertexId> = rustc_hash::FxHashSet::default();
1685        if self.pk_order.is_some() {
1686            if let Some(mut pk) = self.pk_order.take() {
1687                pk.ensure_nodes(std::iter::once(dependent));
1688                pk.ensure_nodes(dependencies.iter().copied());
1689                {
1690                    let adapter = GraphAdapter { g: self };
1691                    for &dep_id in dependencies {
1692                        match pk.try_add_edge(&adapter, dep_id, dependent) {
1693                            Ok(_) => {}
1694                            Err(_cycle) => {
1695                                if self.config.pk_reject_cycle_edges {
1696                                    skip_deps.insert(dep_id);
1697                                } else {
1698                                    pk.rebuild_full(&adapter);
1699                                }
1700                            }
1701                        }
1702                    }
1703                }
1704                self.pk_order = Some(pk);
1705            }
1706        }
1707
1708        for &dep_id in dependencies {
1709            if self.config.pk_reject_cycle_edges && skip_deps.contains(&dep_id) {
1710                continue;
1711            }
1712            self.edges.add_edge(dependent, dep_id);
1713            #[cfg(test)]
1714            {
1715                self.instr.edges_added += 1;
1716            }
1717        }
1718    }
1719
1720    /// Bulk set formulas on a sheet using a single dependency plan and batched edge updates.
1721    pub fn bulk_set_formulas<I>(&mut self, sheet: &str, items: I) -> Result<usize, ExcelError>
1722    where
1723        I: IntoIterator<Item = (u32, u32, ASTNode)>,
1724    {
1725        use formualizer_parse::parser::CollectPolicy;
1726        let sheet_id = self.sheet_id_mut(sheet);
1727
1728        // 1) Collect items to allow a single plan build
1729        let collected: Vec<(u32, u32, ASTNode)> = items.into_iter().collect();
1730        if collected.is_empty() {
1731            return Ok(0);
1732        }
1733
1734        // 2) Build plan across all formulas (read-only, no graph mutation)
1735        let tiny_refs = collected.iter().map(|(r, c, ast)| (sheet, *r, *c, ast));
1736        let vol_flags: Vec<bool> = collected
1737            .iter()
1738            .map(|(_, _, ast)| self.is_ast_volatile(ast))
1739            .collect();
1740        let policy = CollectPolicy {
1741            expand_small_ranges: true,
1742            range_expansion_limit: self.config.range_expansion_limit,
1743            include_names: true,
1744        };
1745        let plan = crate::engine::plan::build_dependency_plan(
1746            &mut self.sheet_reg,
1747            tiny_refs,
1748            &policy,
1749            Some(&vol_flags),
1750        )?;
1751
1752        // 3) Ensure/create target vertices and referenced cells (placeholders) once
1753        let mut created_placeholders: Vec<CellRef> = Vec::new();
1754
1755        // Targets
1756        let mut target_vids: Vec<VertexId> = Vec::with_capacity(plan.formula_targets.len());
1757        for (sid, pc) in &plan.formula_targets {
1758            let addr = CellRef::new(*sid, Coord::new(pc.row(), pc.col(), true, true));
1759            let vid = if let Some(&existing) = self.cell_to_vertex.get(&addr) {
1760                existing
1761            } else {
1762                self.get_or_create_vertex(&addr, &mut created_placeholders)
1763            };
1764            target_vids.push(vid);
1765        }
1766
1767        // Global referenced cells
1768        let mut dep_vids: Vec<VertexId> = Vec::with_capacity(plan.global_cells.len());
1769        for (sid, pc) in &plan.global_cells {
1770            let addr = CellRef::new(*sid, Coord::new(pc.row(), pc.col(), true, true));
1771            let vid = if let Some(&existing) = self.cell_to_vertex.get(&addr) {
1772                existing
1773            } else {
1774                self.get_or_create_vertex(&addr, &mut created_placeholders)
1775            };
1776            dep_vids.push(vid);
1777        }
1778
1779        // 4) Store ASTs in batch and update kinds/flags/value map
1780        let ast_ids = self
1781            .data_store
1782            .store_asts_batch(collected.iter().map(|(_, _, ast)| ast), &self.sheet_reg);
1783
1784        for (i, &tvid) in target_vids.iter().enumerate() {
1785            // If this cell already had a formula, remove its edges once here
1786            if self.vertex_formulas.contains_key(&tvid) {
1787                self.remove_dependent_edges(tvid);
1788            }
1789            self.store.set_kind(tvid, VertexKind::FormulaScalar);
1790            self.store.set_dirty(tvid, true);
1791            self.vertex_values.remove(&tvid);
1792            self.vertex_formulas.insert(tvid, ast_ids[i]);
1793            if vol_flags.get(i).copied().unwrap_or(false) {
1794                self.store.set_volatile(tvid, true);
1795                self.volatile_vertices.insert(tvid);
1796            }
1797        }
1798
1799        // 5) Add edges in one batch
1800        self.edges.begin_batch();
1801        for (i, tvid) in target_vids.iter().copied().enumerate() {
1802            // Map per-formula indices into dep_vids
1803            if let Some(indices) = plan.per_formula_cells.get(i) {
1804                let mut deps: Vec<VertexId> = Vec::with_capacity(indices.len());
1805                for &idx in indices {
1806                    if let Some(vid) = dep_vids.get(idx as usize) {
1807                        deps.push(*vid);
1808                    }
1809                }
1810                self.add_dependent_edges_nobatch(tvid, &deps);
1811            }
1812
1813            // Rebuild ranges from RangeKey and add via existing helper
1814            if let Some(rks) = plan.per_formula_ranges.get(i) {
1815                let mut range_refs: Vec<ReferenceType> = Vec::with_capacity(rks.len());
1816                use crate::engine::plan::RangeKey as RK;
1817                for rk in rks {
1818                    match rk {
1819                        RK::Rect { sheet, start, end } => range_refs.push(ReferenceType::Range {
1820                            sheet: Some(self.sheet_name(*sheet).to_string()),
1821                            start_row: Some(start.row()),
1822                            start_col: Some(start.col()),
1823                            end_row: Some(end.row()),
1824                            end_col: Some(end.col()),
1825                        }),
1826                        RK::WholeRow { sheet, row } => range_refs.push(ReferenceType::Range {
1827                            sheet: Some(self.sheet_name(*sheet).to_string()),
1828                            start_row: Some(*row),
1829                            start_col: None,
1830                            end_row: Some(*row),
1831                            end_col: None,
1832                        }),
1833                        RK::WholeCol { sheet, col } => range_refs.push(ReferenceType::Range {
1834                            sheet: Some(self.sheet_name(*sheet).to_string()),
1835                            start_row: None,
1836                            start_col: Some(*col),
1837                            end_row: None,
1838                            end_col: Some(*col),
1839                        }),
1840                        RK::OpenRect { sheet, start, end } => {
1841                            range_refs.push(ReferenceType::Range {
1842                                sheet: Some(self.sheet_name(*sheet).to_string()),
1843                                start_row: start.map(|p| p.row()),
1844                                start_col: start.map(|p| p.col()),
1845                                end_row: end.map(|p| p.row()),
1846                                end_col: end.map(|p| p.col()),
1847                            })
1848                        }
1849                    }
1850                }
1851                self.add_range_dependent_edges(tvid, &range_refs, sheet_id);
1852            }
1853        }
1854        self.edges.end_batch();
1855
1856        Ok(collected.len())
1857    }
1858
1859    /// Public (crate) helper to add a single dependency edge (dependent -> dependency) used for restoration/undo.
1860    pub fn add_dependency_edge(&mut self, dependent: VertexId, dependency: VertexId) {
1861        if dependent == dependency {
1862            return;
1863        }
1864        // If PK enabled attempt to add maintaining ordering; fallback to rebuild if cycle
1865        if self.pk_order.is_some() {
1866            if let Some(mut pk) = self.pk_order.take() {
1867                pk.ensure_nodes(std::iter::once(dependent));
1868                pk.ensure_nodes(std::iter::once(dependency));
1869                let adapter = GraphAdapter { g: self };
1870                if pk.try_add_edge(&adapter, dependency, dependent).is_err() {
1871                    // Cycle: rebuild full (conservative)
1872                    pk.rebuild_full(&adapter);
1873                }
1874                self.pk_order = Some(pk);
1875            }
1876        }
1877        self.edges.add_edge(dependent, dependency);
1878        self.store.set_dirty(dependent, true);
1879        self.dirty_vertices.insert(dependent);
1880    }
1881
1882    fn add_range_dependent_edges(
1883        &mut self,
1884        dependent: VertexId,
1885        ranges: &[ReferenceType],
1886        current_sheet_id: SheetId,
1887    ) {
1888        if ranges.is_empty() {
1889            return;
1890        }
1891        self.formula_to_range_deps
1892            .insert(dependent, ranges.to_vec());
1893
1894        for range in ranges {
1895            if let ReferenceType::Range {
1896                sheet,
1897                start_row,
1898                start_col,
1899                end_row,
1900                end_col,
1901            } = range
1902            {
1903                let sheet_id = match sheet {
1904                    Some(name) => self.sheet_id_mut(name),
1905                    None => current_sheet_id,
1906                };
1907                let s_row = *start_row;
1908                let e_row = *end_row;
1909                let s_col = *start_col;
1910                let e_col = *end_col;
1911
1912                // Decide coarse stripes for invalidation
1913                let col_stripes = (s_row.is_none() && e_row.is_none())
1914                    || (s_col.is_some() && e_col.is_some() && (s_row.is_none() || e_row.is_none())); // partial rows, fixed columns
1915                let row_stripes = (s_col.is_none() && e_col.is_none())
1916                    || (s_row.is_some() && e_row.is_some() && (s_col.is_none() || e_col.is_none())); // partial cols, fixed rows
1917
1918                if col_stripes && !row_stripes {
1919                    let sc = s_col.unwrap_or(1);
1920                    let ec = e_col.unwrap_or(sc);
1921                    for col in sc..=ec {
1922                        let key = StripeKey {
1923                            sheet_id,
1924                            stripe_type: StripeType::Column,
1925                            index: col,
1926                        };
1927                        self.stripe_to_dependents
1928                            .entry(key.clone())
1929                            .or_default()
1930                            .insert(dependent);
1931                        #[cfg(test)]
1932                        {
1933                            // Count only when inserting into an empty set
1934                            if self.stripe_to_dependents.get(&key).map(|s| s.len()) == Some(1) {
1935                                self.instr.stripe_inserts += 1;
1936                            }
1937                        }
1938                    }
1939                    continue;
1940                }
1941
1942                if row_stripes && !col_stripes {
1943                    let sr = s_row.unwrap_or(1);
1944                    let er = e_row.unwrap_or(sr);
1945                    for row in sr..=er {
1946                        let key = StripeKey {
1947                            sheet_id,
1948                            stripe_type: StripeType::Row,
1949                            index: row,
1950                        };
1951                        self.stripe_to_dependents
1952                            .entry(key.clone())
1953                            .or_default()
1954                            .insert(dependent);
1955                        #[cfg(test)]
1956                        {
1957                            if self.stripe_to_dependents.get(&key).map(|s| s.len()) == Some(1) {
1958                                self.instr.stripe_inserts += 1;
1959                            }
1960                        }
1961                    }
1962                    continue;
1963                }
1964
1965                // Finite rectangle (or ambiguous): fall back to block/row/col heuristic
1966                let start_row = s_row.unwrap_or(1);
1967                let start_col = s_col.unwrap_or(1);
1968                let end_row = e_row.unwrap_or(start_row);
1969                let end_col = e_col.unwrap_or(start_col);
1970
1971                let height = end_row.saturating_sub(start_row) + 1;
1972                let width = end_col.saturating_sub(start_col) + 1;
1973
1974                if self.config.enable_block_stripes && height > 1 && width > 1 {
1975                    let start_block_row = start_row / BLOCK_H;
1976                    let end_block_row = end_row / BLOCK_H;
1977                    let start_block_col = start_col / BLOCK_W;
1978                    let end_block_col = end_col / BLOCK_W;
1979
1980                    for block_row in start_block_row..=end_block_row {
1981                        for block_col in start_block_col..=end_block_col {
1982                            let key = StripeKey {
1983                                sheet_id,
1984                                stripe_type: StripeType::Block,
1985                                index: block_index(block_row * BLOCK_H, block_col * BLOCK_W),
1986                            };
1987                            self.stripe_to_dependents
1988                                .entry(key.clone())
1989                                .or_default()
1990                                .insert(dependent);
1991                            #[cfg(test)]
1992                            {
1993                                if self.stripe_to_dependents.get(&key).map(|s| s.len()) == Some(1) {
1994                                    self.instr.stripe_inserts += 1;
1995                                }
1996                            }
1997                        }
1998                    }
1999                } else if height > width {
2000                    // Tall range
2001                    for col in start_col..=end_col {
2002                        let key = StripeKey {
2003                            sheet_id,
2004                            stripe_type: StripeType::Column,
2005                            index: col,
2006                        };
2007                        self.stripe_to_dependents
2008                            .entry(key.clone())
2009                            .or_default()
2010                            .insert(dependent);
2011                        #[cfg(test)]
2012                        {
2013                            if self.stripe_to_dependents.get(&key).map(|s| s.len()) == Some(1) {
2014                                self.instr.stripe_inserts += 1;
2015                            }
2016                        }
2017                    }
2018                } else {
2019                    // Wide range
2020                    for row in start_row..=end_row {
2021                        let key = StripeKey {
2022                            sheet_id,
2023                            stripe_type: StripeType::Row,
2024                            index: row,
2025                        };
2026                        self.stripe_to_dependents
2027                            .entry(key.clone())
2028                            .or_default()
2029                            .insert(dependent);
2030                        #[cfg(test)]
2031                        {
2032                            if self.stripe_to_dependents.get(&key).map(|s| s.len()) == Some(1) {
2033                                self.instr.stripe_inserts += 1;
2034                            }
2035                        }
2036                    }
2037                }
2038            }
2039        }
2040    }
2041
2042    /// Fast-path: add range dependencies using compact RangeKey without string conversions.
2043    pub fn add_range_deps_from_keys(
2044        &mut self,
2045        dependent: VertexId,
2046        keys: &[crate::engine::plan::RangeKey],
2047        current_sheet_id: SheetId,
2048    ) {
2049        use crate::engine::plan::RangeKey as RK;
2050        if keys.is_empty() {
2051            return;
2052        }
2053        // Mirror formula_to_range_deps with reconstructed ReferenceType but avoid sheet name lookups in the loop.
2054        let mut ranges: Vec<ReferenceType> = Vec::with_capacity(keys.len());
2055        for k in keys {
2056            match k {
2057                RK::Rect { sheet, start, end } => ranges.push(ReferenceType::Range {
2058                    sheet: Some(self.sheet_name(*sheet).to_string()),
2059                    start_row: Some(start.row()),
2060                    start_col: Some(start.col()),
2061                    end_row: Some(end.row()),
2062                    end_col: Some(end.col()),
2063                }),
2064                RK::WholeRow { sheet, row } => ranges.push(ReferenceType::Range {
2065                    sheet: Some(self.sheet_name(*sheet).to_string()),
2066                    start_row: Some(*row),
2067                    start_col: None,
2068                    end_row: Some(*row),
2069                    end_col: None,
2070                }),
2071                RK::WholeCol { sheet, col } => ranges.push(ReferenceType::Range {
2072                    sheet: Some(self.sheet_name(*sheet).to_string()),
2073                    start_row: None,
2074                    start_col: Some(*col),
2075                    end_row: None,
2076                    end_col: Some(*col),
2077                }),
2078                RK::OpenRect { sheet, start, end } => ranges.push(ReferenceType::Range {
2079                    sheet: Some(self.sheet_name(*sheet).to_string()),
2080                    start_row: start.map(|p| p.row()),
2081                    start_col: start.map(|p| p.col()),
2082                    end_row: end.map(|p| p.row()),
2083                    end_col: end.map(|p| p.col()),
2084                }),
2085            }
2086        }
2087        // Store ranges for formula
2088        self.formula_to_range_deps.insert(dependent, ranges.clone());
2089
2090        // Batch stripe registration
2091        for r in &ranges {
2092            if let ReferenceType::Range {
2093                sheet: _,
2094                start_row,
2095                start_col,
2096                end_row,
2097                end_col,
2098            } = r
2099            {
2100                // Compute stripes as in add_range_dependent_edges but without sheet id mapping (assume current sheet implied)
2101                let s_row = *start_row;
2102                let e_row = *end_row;
2103                let s_col = *start_col;
2104                let e_col = *end_col;
2105                let col_stripes = (s_row.is_none() && e_row.is_none())
2106                    || (s_col.is_some() && e_col.is_some() && (s_row.is_none() || e_row.is_none()));
2107                let row_stripes = (s_col.is_none() && e_col.is_none())
2108                    || (s_row.is_some() && e_row.is_some() && (s_col.is_none() || e_col.is_none()));
2109                if col_stripes && !row_stripes {
2110                    let sc = s_col.unwrap_or(1);
2111                    let ec = e_col.unwrap_or(sc);
2112                    for col in sc..=ec {
2113                        let key = StripeKey {
2114                            sheet_id: current_sheet_id,
2115                            stripe_type: StripeType::Column,
2116                            index: col,
2117                        };
2118                        self.stripe_to_dependents
2119                            .entry(key)
2120                            .or_default()
2121                            .insert(dependent);
2122                    }
2123                    continue;
2124                }
2125                if row_stripes && !col_stripes {
2126                    let sr = s_row.unwrap_or(1);
2127                    let er = e_row.unwrap_or(sr);
2128                    for row in sr..=er {
2129                        let key = StripeKey {
2130                            sheet_id: current_sheet_id,
2131                            stripe_type: StripeType::Row,
2132                            index: row,
2133                        };
2134                        self.stripe_to_dependents
2135                            .entry(key)
2136                            .or_default()
2137                            .insert(dependent);
2138                    }
2139                    continue;
2140                }
2141                // Block stripes for bounded rects
2142                if let (Some(sr), Some(sc), Some(er), Some(ec)) = (s_row, s_col, e_row, e_col) {
2143                    let key = StripeKey {
2144                        sheet_id: current_sheet_id,
2145                        stripe_type: StripeType::Block,
2146                        index: 0,
2147                    };
2148                    self.stripe_to_dependents
2149                        .entry(key)
2150                        .or_default()
2151                        .insert(dependent);
2152                }
2153            }
2154        }
2155    }
2156
2157    fn remove_dependent_edges(&mut self, vertex: VertexId) {
2158        // Remove all outgoing edges from this vertex (its dependencies)
2159        let dependencies = self.edges.out_edges(vertex);
2160
2161        self.edges.begin_batch();
2162        if self.pk_order.is_some() {
2163            if let Some(mut pk) = self.pk_order.take() {
2164                for dep in &dependencies {
2165                    pk.remove_edge(*dep, vertex);
2166                }
2167                self.pk_order = Some(pk);
2168            }
2169        }
2170        for dep in dependencies {
2171            self.edges.remove_edge(vertex, dep);
2172        }
2173        self.edges.end_batch();
2174
2175        // Remove range dependencies and clean up stripes
2176        if let Some(old_ranges) = self.formula_to_range_deps.remove(&vertex) {
2177            let old_sheet_id = self.store.sheet_id(vertex);
2178
2179            for range in &old_ranges {
2180                if let ReferenceType::Range {
2181                    sheet,
2182                    start_row,
2183                    start_col,
2184                    end_row,
2185                    end_col,
2186                } = range
2187                {
2188                    let sheet_id = match sheet {
2189                        Some(name) => self.sheet_reg.get_id(name).unwrap_or(old_sheet_id),
2190                        None => old_sheet_id,
2191                    };
2192                    let s_row = *start_row;
2193                    let e_row = *end_row;
2194                    let s_col = *start_col;
2195                    let e_col = *end_col;
2196
2197                    let mut keys_to_clean = FxHashSet::default();
2198
2199                    let col_stripes = (s_row.is_none() && e_row.is_none())
2200                        || (s_col.is_some()
2201                            && e_col.is_some()
2202                            && (s_row.is_none() || e_row.is_none()));
2203                    let row_stripes = (s_col.is_none() && e_col.is_none())
2204                        || (s_row.is_some()
2205                            && e_row.is_some()
2206                            && (s_col.is_none() || e_col.is_none()));
2207
2208                    if col_stripes && !row_stripes {
2209                        let sc = s_col.unwrap_or(1);
2210                        let ec = e_col.unwrap_or(sc);
2211                        for col in sc..=ec {
2212                            keys_to_clean.insert(StripeKey {
2213                                sheet_id,
2214                                stripe_type: StripeType::Column,
2215                                index: col,
2216                            });
2217                        }
2218                    } else if row_stripes && !col_stripes {
2219                        let sr = s_row.unwrap_or(1);
2220                        let er = e_row.unwrap_or(sr);
2221                        for row in sr..=er {
2222                            keys_to_clean.insert(StripeKey {
2223                                sheet_id,
2224                                stripe_type: StripeType::Row,
2225                                index: row,
2226                            });
2227                        }
2228                    } else {
2229                        let start_row = s_row.unwrap_or(1);
2230                        let start_col = s_col.unwrap_or(1);
2231                        let end_row = e_row.unwrap_or(start_row);
2232                        let end_col = e_col.unwrap_or(start_col);
2233
2234                        let height = end_row.saturating_sub(start_row) + 1;
2235                        let width = end_col.saturating_sub(start_col) + 1;
2236
2237                        if self.config.enable_block_stripes && height > 1 && width > 1 {
2238                            let start_block_row = start_row / BLOCK_H;
2239                            let end_block_row = end_row / BLOCK_H;
2240                            let start_block_col = start_col / BLOCK_W;
2241                            let end_block_col = end_col / BLOCK_W;
2242
2243                            for block_row in start_block_row..=end_block_row {
2244                                for block_col in start_block_col..=end_block_col {
2245                                    keys_to_clean.insert(StripeKey {
2246                                        sheet_id,
2247                                        stripe_type: StripeType::Block,
2248                                        index: block_index(
2249                                            block_row * BLOCK_H,
2250                                            block_col * BLOCK_W,
2251                                        ),
2252                                    });
2253                                }
2254                            }
2255                        } else if height > width {
2256                            // Tall range
2257                            for col in start_col..=end_col {
2258                                keys_to_clean.insert(StripeKey {
2259                                    sheet_id,
2260                                    stripe_type: StripeType::Column,
2261                                    index: col,
2262                                });
2263                            }
2264                        } else {
2265                            // Wide range
2266                            for row in start_row..=end_row {
2267                                keys_to_clean.insert(StripeKey {
2268                                    sheet_id,
2269                                    stripe_type: StripeType::Row,
2270                                    index: row,
2271                                });
2272                            }
2273                        }
2274                    }
2275
2276                    for key in keys_to_clean {
2277                        if let Some(dependents) = self.stripe_to_dependents.get_mut(&key) {
2278                            dependents.remove(&vertex);
2279                            if dependents.is_empty() {
2280                                self.stripe_to_dependents.remove(&key);
2281                                #[cfg(test)]
2282                                {
2283                                    self.instr.stripe_removes += 1;
2284                                }
2285                            }
2286                        }
2287                    }
2288                }
2289            }
2290        }
2291    }
2292
2293    // Removed: vertices() and get_vertex() methods - no longer needed with SoA
2294    // The old AoS Vertex struct has been eliminated in favor of direct
2295    // access to columnar data through the VertexStore
2296
2297    /// Updates the cached value of a formula vertex.
2298    pub(crate) fn update_vertex_value(&mut self, vertex_id: VertexId, value: LiteralValue) {
2299        let value_ref = self.data_store.store_value(value);
2300        self.vertex_values.insert(vertex_id, value_ref);
2301    }
2302
2303    /// Plan a spill region for an anchor; returns #SPILL! if blocked
2304    pub fn plan_spill_region(
2305        &self,
2306        anchor: VertexId,
2307        target_cells: &[CellRef],
2308    ) -> Result<(), ExcelError> {
2309        use formualizer_common::{ExcelErrorExtra, ExcelErrorKind};
2310        // Compute expected spill shape from the target rectangle for better diagnostics
2311        let (expected_rows, expected_cols) = if target_cells.is_empty() {
2312            (0u32, 0u32)
2313        } else {
2314            let mut min_r = u32::MAX;
2315            let mut max_r = 0u32;
2316            let mut min_c = u32::MAX;
2317            let mut max_c = 0u32;
2318            for cell in target_cells {
2319                let r = cell.coord.row;
2320                let c = cell.coord.col;
2321                if r < min_r {
2322                    min_r = r;
2323                }
2324                if r > max_r {
2325                    max_r = r;
2326                }
2327                if c < min_c {
2328                    min_c = c;
2329                }
2330                if c > max_c {
2331                    max_c = c;
2332                }
2333            }
2334            (
2335                max_r.saturating_sub(min_r).saturating_add(1),
2336                max_c.saturating_sub(min_c).saturating_add(1),
2337            )
2338        };
2339        // Allow overlapping with previously owned spill cells by this anchor
2340        for cell in target_cells {
2341            // If cell is already owned by this anchor's previous spill, it's allowed.
2342            let owned_by_anchor = match self.spill_cell_to_anchor.get(cell) {
2343                Some(&existing_anchor) if existing_anchor == anchor => true,
2344                Some(_other) => {
2345                    return Err(ExcelError::new(ExcelErrorKind::Spill)
2346                        .with_message("BlockedBySpill")
2347                        .with_extra(ExcelErrorExtra::Spill {
2348                            expected_rows,
2349                            expected_cols,
2350                        }));
2351                }
2352                None => false,
2353            };
2354
2355            if owned_by_anchor {
2356                continue;
2357            }
2358
2359            // If cell is occupied by another formula anchor, block regardless of value visibility
2360            if let Some(&vid) = self.cell_to_vertex.get(cell) {
2361                if vid != anchor {
2362                    // Prevent clobbering formulas (array or scalar) in the target area
2363                    match self.store.kind(vid) {
2364                        VertexKind::FormulaScalar | VertexKind::FormulaArray => {
2365                            return Err(ExcelError::new(ExcelErrorKind::Spill)
2366                                .with_message("BlockedByFormula")
2367                                .with_extra(ExcelErrorExtra::Spill {
2368                                    expected_rows,
2369                                    expected_cols,
2370                                }));
2371                        }
2372                        _ => {
2373                            // If a non-empty value exists (and not this anchor), block
2374                            if let Some(vref) = self.vertex_values.get(&vid) {
2375                                let v = self.data_store.retrieve_value(*vref);
2376                                if !matches!(v, LiteralValue::Empty) {
2377                                    return Err(ExcelError::new(ExcelErrorKind::Spill)
2378                                        .with_message("BlockedByValue")
2379                                        .with_extra(ExcelErrorExtra::Spill {
2380                                            expected_rows,
2381                                            expected_cols,
2382                                        }));
2383                                }
2384                            }
2385                        }
2386                    }
2387                }
2388            }
2389        }
2390        Ok(())
2391    }
2392
2393    // Note: non-atomic commit_spill_region has been removed. All callers must use
2394    // commit_spill_region_atomic_with_fault for atomicity and rollback on failure.
2395
2396    /// Commit a spill atomically with an internal shadow buffer and optional fault injection.
2397    /// If a fault is injected partway through, all changes are rolled back to the pre-commit state.
2398    /// This does not change behavior under normal operation; it's primarily for Phase 3 guarantees and tests.
2399    pub fn commit_spill_region_atomic_with_fault(
2400        &mut self,
2401        anchor: VertexId,
2402        target_cells: Vec<CellRef>,
2403        values: Vec<Vec<LiteralValue>>,
2404        fault_after_ops: Option<usize>,
2405    ) -> Result<(), ExcelError> {
2406        use rustc_hash::FxHashSet;
2407
2408        // Capture previous owned cells for this anchor
2409        let prev_cells = self
2410            .spill_anchor_to_cells
2411            .get(&anchor)
2412            .cloned()
2413            .unwrap_or_default();
2414        let new_set: FxHashSet<CellRef> = target_cells.iter().copied().collect();
2415        let prev_set: FxHashSet<CellRef> = prev_cells.iter().copied().collect();
2416
2417        // Compose operation list: clears first (prev - new), then writes for new rectangle
2418        #[derive(Clone)]
2419        struct Op {
2420            sheet: String,
2421            row: u32,
2422            col: u32,
2423            new_value: LiteralValue,
2424        }
2425        let mut ops: Vec<Op> = Vec::new();
2426
2427        // Clears for cells no longer used
2428        for cell in prev_cells.iter() {
2429            if !new_set.contains(cell) {
2430                let sheet = self.sheet_name(cell.sheet_id).to_string();
2431                ops.push(Op {
2432                    sheet,
2433                    row: cell.coord.row,
2434                    col: cell.coord.col,
2435                    new_value: LiteralValue::Empty,
2436                });
2437            }
2438        }
2439
2440        // Writes for new values (row-major to match target rectangle)
2441        if !target_cells.is_empty() {
2442            let first = target_cells.first().copied().unwrap();
2443            let row0 = first.coord.row;
2444            let col0 = first.coord.col;
2445            let sheet = self.sheet_name(first.sheet_id).to_string();
2446            for (r_off, row_vals) in values.iter().enumerate() {
2447                for (c_off, v) in row_vals.iter().enumerate() {
2448                    ops.push(Op {
2449                        sheet: sheet.clone(),
2450                        row: row0 + r_off as u32,
2451                        col: col0 + c_off as u32,
2452                        new_value: v.clone(),
2453                    });
2454                }
2455            }
2456        }
2457
2458        // Shadow buffer of old values for rollback
2459        #[derive(Clone)]
2460        struct OldVal {
2461            present: bool,
2462            value: LiteralValue,
2463        }
2464        let mut old_values: Vec<((String, u32, u32), OldVal)> = Vec::with_capacity(ops.len());
2465
2466        // Capture old values before applying
2467        for op in &ops {
2468            let old = self
2469                .get_cell_value(&op.sheet, op.row, op.col)
2470                .unwrap_or(LiteralValue::Empty);
2471            let present = true; // unified model: we always treat as present
2472            old_values.push((
2473                (op.sheet.clone(), op.row, op.col),
2474                OldVal {
2475                    present,
2476                    value: old,
2477                },
2478            ));
2479        }
2480
2481        // Apply with optional injected fault
2482        for (applied, op) in ops.iter().enumerate() {
2483            if let Some(n) = fault_after_ops {
2484                if applied == n {
2485                    for idx in (0..applied).rev() {
2486                        let ((ref sheet, row, col), ref old) = old_values[idx];
2487                        let _ = self.set_cell_value(sheet, row, col, old.value.clone());
2488                    }
2489                    return Err(ExcelError::new(ExcelErrorKind::Error)
2490                        .with_message("Injected persistence fault during spill commit"));
2491                }
2492            }
2493            let _ = self.set_cell_value(&op.sheet, op.row, op.col, op.new_value.clone());
2494        }
2495
2496        // Update spill ownership maps only on success
2497        // Clear previous ownership not reused
2498        for cell in prev_cells.iter() {
2499            if !new_set.contains(cell) {
2500                self.spill_cell_to_anchor.remove(cell);
2501            }
2502        }
2503        // Mark ownership for new rectangle using the declared target cells only
2504        for cell in &target_cells {
2505            self.spill_cell_to_anchor.insert(*cell, anchor);
2506        }
2507        self.spill_anchor_to_cells.insert(anchor, target_cells);
2508        Ok(())
2509    }
2510
2511    /// Clear an existing spill region for an anchor (set cells to Empty and forget ownership)
2512    pub fn clear_spill_region(&mut self, anchor: VertexId) {
2513        if let Some(cells) = self.spill_anchor_to_cells.remove(&anchor) {
2514            for cell in cells {
2515                let sheet = self.sheet_name(cell.sheet_id).to_string();
2516                let _ = self.set_cell_value(
2517                    &sheet,
2518                    cell.coord.row,
2519                    cell.coord.col,
2520                    LiteralValue::Empty,
2521                );
2522                self.spill_cell_to_anchor.remove(&cell);
2523            }
2524        }
2525    }
2526
2527    /// Check if a vertex exists
2528    pub(crate) fn vertex_exists(&self, vertex_id: VertexId) -> bool {
2529        if vertex_id.0 < FIRST_NORMAL_VERTEX {
2530            return false;
2531        }
2532        let index = (vertex_id.0 - FIRST_NORMAL_VERTEX) as usize;
2533        index < self.store.len()
2534    }
2535
2536    /// Get the kind of a vertex
2537    pub(crate) fn get_vertex_kind(&self, vertex_id: VertexId) -> VertexKind {
2538        self.store.kind(vertex_id)
2539    }
2540
2541    /// Get the sheet ID of a vertex
2542    pub(crate) fn get_vertex_sheet_id(&self, vertex_id: VertexId) -> SheetId {
2543        self.store.sheet_id(vertex_id)
2544    }
2545
2546    /// Get the formula AST for a vertex
2547    pub fn get_formula(&self, vertex_id: VertexId) -> Option<ASTNode> {
2548        self.vertex_formulas
2549            .get(&vertex_id)
2550            .and_then(|&ast_id| self.data_store.retrieve_ast(ast_id, &self.sheet_reg))
2551    }
2552
2553    /// Get the value stored for a vertex
2554    pub fn get_value(&self, vertex_id: VertexId) -> Option<LiteralValue> {
2555        self.vertex_values
2556            .get(&vertex_id)
2557            .map(|&value_ref| self.data_store.retrieve_value(value_ref))
2558    }
2559
2560    /// Get the cell reference for a vertex
2561    pub(crate) fn get_cell_ref(&self, vertex_id: VertexId) -> Option<CellRef> {
2562        let packed_coord = self.store.coord(vertex_id);
2563        let sheet_id = self.store.sheet_id(vertex_id);
2564        let coord = Coord::new(packed_coord.row(), packed_coord.col(), true, true);
2565        Some(CellRef::new(sheet_id, coord))
2566    }
2567
2568    /// Create a cell reference (helper for internal use)
2569    pub(crate) fn make_cell_ref_internal(&self, sheet_id: SheetId, row: u32, col: u32) -> CellRef {
2570        let coord = Coord::new(row, col, true, true);
2571        CellRef::new(sheet_id, coord)
2572    }
2573
2574    /// Create a cell reference from sheet name and coordinates
2575    pub fn make_cell_ref(&self, sheet_name: &str, row: u32, col: u32) -> CellRef {
2576        let sheet_id = self.sheet_reg.get_id(sheet_name).unwrap_or(0);
2577        self.make_cell_ref_internal(sheet_id, row, col)
2578    }
2579
2580    /// Check if a vertex is dirty
2581    pub(crate) fn is_dirty(&self, vertex_id: VertexId) -> bool {
2582        self.store.is_dirty(vertex_id)
2583    }
2584
2585    /// Check if a vertex is volatile
2586    pub(crate) fn is_volatile(&self, vertex_id: VertexId) -> bool {
2587        self.store.is_volatile(vertex_id)
2588    }
2589
2590    /// Get vertex ID for a cell address
2591    pub fn get_vertex_id_for_address(&self, addr: &CellRef) -> Option<&VertexId> {
2592        self.cell_to_vertex.get(addr)
2593    }
2594
2595    #[cfg(test)]
2596    pub fn cell_to_vertex(&self) -> &FxHashMap<CellRef, VertexId> {
2597        &self.cell_to_vertex
2598    }
2599
2600    /// Get the dependencies of a vertex (for scheduler)
2601    pub(crate) fn get_dependencies(&self, vertex_id: VertexId) -> Vec<VertexId> {
2602        self.edges.out_edges(vertex_id)
2603    }
2604
2605    /// Check if a vertex has a self-loop
2606    pub(crate) fn has_self_loop(&self, vertex_id: VertexId) -> bool {
2607        self.edges.out_edges(vertex_id).contains(&vertex_id)
2608    }
2609
2610    /// Get dependents of a vertex (vertices that depend on this vertex)
2611    /// Uses reverse edges for O(1) lookup when available
2612    pub(crate) fn get_dependents(&self, vertex_id: VertexId) -> Vec<VertexId> {
2613        // If there are pending changes in delta, we need to scan
2614        // Otherwise we can use the fast reverse edges
2615        if self.edges.delta_size() > 0 {
2616            // Fall back to scanning when delta has changes
2617            let mut dependents = Vec::new();
2618            for (&_addr, &vid) in &self.cell_to_vertex {
2619                let out_edges = self.edges.out_edges(vid);
2620                if out_edges.contains(&vertex_id) {
2621                    dependents.push(vid);
2622                }
2623            }
2624            dependents
2625        } else {
2626            // Fast path: use reverse edges from CSR
2627            self.edges.in_edges(vertex_id).to_vec()
2628        }
2629    }
2630
2631    // Internal helper methods for Milestone 0.4
2632
2633    /// Internal: Create a snapshot of vertex state for rollback
2634    #[doc(hidden)]
2635    pub fn snapshot_vertex(&self, id: VertexId) -> crate::engine::VertexSnapshot {
2636        let coord = self.store.coord(id);
2637        let sheet_id = self.store.sheet_id(id);
2638        let kind = self.store.kind(id);
2639        let flags = self.store.flags(id);
2640
2641        // Get value and formula references
2642        let value_ref = self.vertex_values.get(&id).copied();
2643        let formula_ref = self.vertex_formulas.get(&id).copied();
2644
2645        // Get outgoing edges (dependencies)
2646        let out_edges = self.get_dependencies(id);
2647
2648        crate::engine::VertexSnapshot {
2649            coord,
2650            sheet_id,
2651            kind,
2652            flags,
2653            value_ref,
2654            formula_ref,
2655            out_edges,
2656        }
2657    }
2658
2659    /// Internal: Remove all edges for a vertex
2660    #[doc(hidden)]
2661    pub fn remove_all_edges(&mut self, id: VertexId) {
2662        // Enter batch mode to avoid intermediate rebuilds
2663        self.edges.begin_batch();
2664
2665        // Remove outgoing edges (this vertex's dependencies)
2666        self.remove_dependent_edges(id);
2667
2668        // Force rebuild to get accurate dependents list
2669        // This is necessary because get_dependents uses CSR reverse edges
2670        self.edges.rebuild();
2671
2672        // Remove incoming edges (vertices that depend on this vertex)
2673        let dependents = self.get_dependents(id);
2674        if self.pk_order.is_some() {
2675            if let Some(mut pk) = self.pk_order.take() {
2676                for dependent in &dependents {
2677                    pk.remove_edge(id, *dependent);
2678                }
2679                self.pk_order = Some(pk);
2680            }
2681        }
2682        for dependent in dependents {
2683            self.edges.remove_edge(dependent, id);
2684        }
2685
2686        // Exit batch mode and rebuild once with all changes
2687        self.edges.end_batch();
2688    }
2689
2690    /// Internal: Mark vertex as having #REF! error
2691    #[doc(hidden)]
2692    pub fn mark_as_ref_error(&mut self, id: VertexId) {
2693        let error = LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref));
2694        let value_ref = self.data_store.store_value(error);
2695        self.vertex_values.insert(id, value_ref);
2696        self.store.set_dirty(id, true);
2697        self.dirty_vertices.insert(id);
2698    }
2699
2700    /// Check if a vertex has a #REF! error
2701    pub fn is_ref_error(&self, id: VertexId) -> bool {
2702        if let Some(value_ref) = self.vertex_values.get(&id) {
2703            let value = self.data_store.retrieve_value(*value_ref);
2704            if let LiteralValue::Error(err) = value {
2705                return err.kind == ExcelErrorKind::Ref;
2706            }
2707        }
2708        false
2709    }
2710
2711    /// Internal: Mark all direct dependents as dirty
2712    #[doc(hidden)]
2713    pub fn mark_dependents_dirty(&mut self, id: VertexId) {
2714        let dependents = self.get_dependents(id);
2715        for dep_id in dependents {
2716            self.store.set_dirty(dep_id, true);
2717            self.dirty_vertices.insert(dep_id);
2718        }
2719    }
2720
2721    /// Internal: Mark a vertex as volatile
2722    #[doc(hidden)]
2723    pub fn mark_volatile(&mut self, id: VertexId, volatile: bool) {
2724        self.store.set_volatile(id, volatile);
2725        if volatile {
2726            self.volatile_vertices.insert(id);
2727        } else {
2728            self.volatile_vertices.remove(&id);
2729        }
2730    }
2731
2732    /// Update vertex coordinate
2733    #[doc(hidden)]
2734    pub fn set_coord(&mut self, id: VertexId, coord: PackedCoord) {
2735        self.store.set_coord(id, coord);
2736    }
2737
2738    /// Update edge cache coordinate
2739    #[doc(hidden)]
2740    pub fn update_edge_coord(&mut self, id: VertexId, coord: PackedCoord) {
2741        self.edges.update_coord(id, coord);
2742    }
2743
2744    /// Mark vertex as deleted (tombstone)
2745    #[doc(hidden)]
2746    pub fn mark_deleted(&mut self, id: VertexId, deleted: bool) {
2747        self.store.mark_deleted(id, deleted);
2748    }
2749
2750    /// Set vertex kind
2751    #[doc(hidden)]
2752    pub fn set_kind(&mut self, id: VertexId, kind: VertexKind) {
2753        self.store.set_kind(id, kind);
2754    }
2755
2756    /// Set vertex dirty flag
2757    #[doc(hidden)]
2758    pub fn set_dirty(&mut self, id: VertexId, dirty: bool) {
2759        self.store.set_dirty(id, dirty);
2760        if dirty {
2761            self.dirty_vertices.insert(id);
2762        } else {
2763            self.dirty_vertices.remove(&id);
2764        }
2765    }
2766
2767    /// Get vertex kind (for testing)
2768    #[cfg(test)]
2769    pub(crate) fn get_kind(&self, id: VertexId) -> VertexKind {
2770        self.store.kind(id)
2771    }
2772
2773    /// Get vertex flags (for testing)
2774    #[cfg(test)]
2775    pub(crate) fn get_flags(&self, id: VertexId) -> u8 {
2776        self.store.flags(id)
2777    }
2778
2779    /// Check if vertex is deleted (for testing)
2780    #[cfg(test)]
2781    pub(crate) fn is_deleted(&self, id: VertexId) -> bool {
2782        self.store.is_deleted(id)
2783    }
2784
2785    /// Force edge rebuild (internal use)
2786    #[doc(hidden)]
2787    pub fn rebuild_edges(&mut self) {
2788        self.edges.rebuild();
2789    }
2790
2791    /// Get delta size (internal use)
2792    #[doc(hidden)]
2793    pub fn edges_delta_size(&self) -> usize {
2794        self.edges.delta_size()
2795    }
2796
2797    /// Get vertex ID for specific cell address
2798    pub fn get_vertex_for_cell(&self, addr: &CellRef) -> Option<VertexId> {
2799        self.cell_to_vertex.get(addr).copied()
2800    }
2801
2802    /// Get coord for a vertex (public for VertexEditor)
2803    pub fn get_coord(&self, id: VertexId) -> PackedCoord {
2804        self.store.coord(id)
2805    }
2806
2807    /// Get sheet_id for a vertex (public for VertexEditor)
2808    pub fn get_sheet_id(&self, id: VertexId) -> SheetId {
2809        self.store.sheet_id(id)
2810    }
2811
2812    /// Get all vertices in a sheet
2813    pub fn vertices_in_sheet(&self, sheet_id: SheetId) -> impl Iterator<Item = VertexId> + '_ {
2814        self.store
2815            .all_vertices()
2816            .filter(move |&id| self.vertex_exists(id) && self.store.sheet_id(id) == sheet_id)
2817    }
2818
2819    /// Does a vertex have a formula associated
2820    pub fn vertex_has_formula(&self, id: VertexId) -> bool {
2821        self.vertex_formulas.contains_key(&id)
2822    }
2823
2824    /// Get all vertices with formulas
2825    pub fn vertices_with_formulas(&self) -> impl Iterator<Item = VertexId> + '_ {
2826        self.vertex_formulas.keys().copied()
2827    }
2828
2829    /// Update a vertex's formula
2830    pub fn update_vertex_formula(&mut self, id: VertexId, ast: ASTNode) -> Result<(), ExcelError> {
2831        // Get the sheet_id for this vertex
2832        let sheet_id = self.store.sheet_id(id);
2833
2834        // If the adjusted AST contains special #REF markers (from structural edits),
2835        // treat this as a REF error on the vertex instead of attempting to resolve.
2836        // This prevents failures when reference_adjuster injected placeholder refs.
2837        let has_ref_marker = ast.get_dependencies().into_iter().any(|r| {
2838            matches!(
2839                r,
2840                ReferenceType::Cell { sheet: Some(s), .. }
2841                    | ReferenceType::Range { sheet: Some(s), .. } if s == "#REF"
2842            )
2843        });
2844        if has_ref_marker {
2845            // Store the adjusted AST for round-tripping/display, but set value state to #REF!
2846            let ast_id = self.data_store.store_ast(&ast, &self.sheet_reg);
2847            self.vertex_formulas.insert(id, ast_id);
2848            self.mark_as_ref_error(id);
2849            self.store.set_kind(id, VertexKind::FormulaScalar);
2850            return Ok(());
2851        }
2852
2853        // Extract dependencies from AST
2854        let (new_dependencies, new_range_dependencies, _) =
2855            self.extract_dependencies(&ast, sheet_id)?;
2856
2857        // Remove old dependencies first
2858        self.remove_dependent_edges(id);
2859
2860        // Store the new formula
2861        let ast_id = self.data_store.store_ast(&ast, &self.sheet_reg);
2862        self.vertex_formulas.insert(id, ast_id);
2863
2864        // Add new dependency edges
2865        self.add_dependent_edges(id, &new_dependencies);
2866        self.add_range_dependent_edges(id, &new_range_dependencies, sheet_id);
2867
2868        // Mark as formula vertex
2869        self.store.set_kind(id, VertexKind::FormulaScalar);
2870
2871        Ok(())
2872    }
2873
2874    /// Mark a vertex as dirty without propagation (for VertexEditor)
2875    pub fn mark_vertex_dirty(&mut self, vertex_id: VertexId) {
2876        self.store.set_dirty(vertex_id, true);
2877        self.dirty_vertices.insert(vertex_id);
2878    }
2879
2880    /// Update cell mapping for a vertex (for VertexEditor)
2881    pub fn update_cell_mapping(
2882        &mut self,
2883        id: VertexId,
2884        old_addr: Option<CellRef>,
2885        new_addr: CellRef,
2886    ) {
2887        // Remove old mapping if it exists
2888        if let Some(old) = old_addr {
2889            self.cell_to_vertex.remove(&old);
2890        }
2891        // Add new mapping
2892        self.cell_to_vertex.insert(new_addr, id);
2893    }
2894
2895    /// Remove cell mapping (for VertexEditor)
2896    pub fn remove_cell_mapping(&mut self, addr: &CellRef) {
2897        self.cell_to_vertex.remove(addr);
2898    }
2899
2900    /// Get the cell reference for a vertex
2901    pub fn get_cell_ref_for_vertex(&self, id: VertexId) -> Option<CellRef> {
2902        let coord = self.store.coord(id);
2903        let sheet_id = self.store.sheet_id(id);
2904        // Find the cell reference in the mapping
2905        let cell_ref = CellRef::new(sheet_id, Coord::new(coord.row(), coord.col(), true, true));
2906        // Verify it actually maps to this vertex
2907        if self.cell_to_vertex.get(&cell_ref) == Some(&id) {
2908            Some(cell_ref)
2909        } else {
2910            None
2911        }
2912    }
2913
2914    // ========== Phase 2: Structural Operations ==========
2915
2916    /// Adjust named ranges during row/column operations
2917    pub fn adjust_named_ranges(
2918        &mut self,
2919        operation: &crate::engine::graph::editor::reference_adjuster::ShiftOperation,
2920    ) -> Result<(), ExcelError> {
2921        let adjuster = crate::engine::graph::editor::reference_adjuster::ReferenceAdjuster::new();
2922
2923        // Adjust workbook-scoped names
2924        for named_range in self.named_ranges.values_mut() {
2925            adjust_named_definition(&mut named_range.definition, &adjuster, operation)?;
2926        }
2927
2928        // Adjust sheet-scoped names
2929        for named_range in self.sheet_named_ranges.values_mut() {
2930            adjust_named_definition(&mut named_range.definition, &adjuster, operation)?;
2931        }
2932
2933        Ok(())
2934    }
2935
2936    /// Mark a vertex as having a #NAME! error
2937    pub fn mark_as_name_error(&mut self, vertex_id: VertexId) {
2938        // Mark the vertex as dirty
2939        self.mark_vertex_dirty(vertex_id);
2940        // In a real implementation, we would store the error in the vertex value
2941        // For now, just mark it dirty so it will error on next evaluation
2942    }
2943}
2944
2945// ========== Sheet Management Operations ==========
2946// This impl block contains all sheet-level operations like adding, removing,
2947// renaming, and duplicating sheets. These operations handle the complex task
2948// of maintaining consistency across all graph structures when sheets change.
2949impl DependencyGraph {
2950    /// Add a new sheet to the workbook
2951    ///
2952    /// Creates a new sheet with the given name. If a sheet with this name
2953    /// already exists, returns its ID without error (idempotent operation).
2954    ///
2955    /// # Arguments
2956    /// * `name` - The name for the new sheet
2957    ///
2958    /// # Returns
2959    /// The SheetId of the new or existing sheet
2960    pub fn add_sheet(&mut self, name: &str) -> Result<SheetId, ExcelError> {
2961        // Check if sheet already exists
2962        if let Some(id) = self.sheet_reg.get_id(name) {
2963            // Sheet already exists, return its ID (idempotent)
2964            return Ok(id);
2965        }
2966
2967        // Create new sheet
2968        let sheet_id = self.sheet_reg.id_for(name);
2969
2970        // Initialize sheet index for the new sheet
2971        self.sheet_indexes.entry(sheet_id).or_default();
2972
2973        Ok(sheet_id)
2974    }
2975
2976    /// Remove a sheet from the workbook
2977    ///
2978    /// Removes the specified sheet and all its contents. This operation:
2979    /// - Deletes all vertices (cells/formulas) in the sheet
2980    /// - Updates all formulas that reference the deleted sheet to show #REF! errors
2981    /// - Removes sheet-scoped named ranges
2982    /// - Cleans up all internal structures
2983    ///
2984    /// # Arguments
2985    /// * `sheet_id` - The ID of the sheet to remove
2986    ///
2987    /// # Errors
2988    /// Returns an error if:
2989    /// - The sheet doesn't exist
2990    /// - Trying to remove the last sheet (workbook must have at least one sheet)
2991    pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
2992        // Check if sheet exists
2993        if self.sheet_reg.name(sheet_id).is_empty() {
2994            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Sheet does not exist"));
2995        }
2996
2997        // Prevent removing the last sheet
2998        let sheet_count = self.sheet_reg.all_sheets().len();
2999        if sheet_count <= 1 {
3000            return Err(
3001                ExcelError::new(ExcelErrorKind::Value).with_message("Cannot remove the last sheet")
3002            );
3003        }
3004
3005        // Begin batch operations for efficiency
3006        self.begin_batch();
3007
3008        // Collect all vertices in the sheet to be deleted
3009        let vertices_to_delete: Vec<VertexId> = self.vertices_in_sheet(sheet_id).collect();
3010
3011        // Find all formulas that reference cells in the deleted sheet
3012        let mut formulas_to_update = Vec::new();
3013        for &formula_id in self.vertex_formulas.keys() {
3014            let deps = self.edges.out_edges(formula_id);
3015            for dep_id in deps {
3016                if self.store.sheet_id(dep_id) == sheet_id {
3017                    formulas_to_update.push(formula_id);
3018                    break;
3019                }
3020            }
3021        }
3022
3023        // Mark formulas that reference the deleted sheet with #REF! errors
3024        for formula_id in formulas_to_update {
3025            self.mark_as_ref_error(formula_id);
3026        }
3027
3028        // Delete all vertices in the sheet
3029        for vertex_id in vertices_to_delete {
3030            // Remove from cell mapping
3031            if let Some(cell_ref) = self.get_cell_ref_for_vertex(vertex_id) {
3032                self.cell_to_vertex.remove(&cell_ref);
3033            }
3034
3035            // Remove all edges
3036            self.remove_all_edges(vertex_id);
3037
3038            // Remove from sheet index
3039            let coord = self.store.coord(vertex_id);
3040            if let Some(index) = self.sheet_indexes.get_mut(&sheet_id) {
3041                index.remove_vertex(coord, vertex_id);
3042            }
3043
3044            // Remove formula and value data
3045            self.vertex_formulas.remove(&vertex_id);
3046            self.vertex_values.remove(&vertex_id);
3047
3048            // Mark as deleted in store
3049            self.mark_deleted(vertex_id, true);
3050        }
3051
3052        // Remove sheet-scoped named ranges
3053        let sheet_names_to_remove: Vec<(SheetId, String)> = self
3054            .sheet_named_ranges
3055            .keys()
3056            .filter(|(sid, _)| *sid == sheet_id)
3057            .cloned()
3058            .collect();
3059
3060        for key in sheet_names_to_remove {
3061            self.sheet_named_ranges.remove(&key);
3062        }
3063
3064        // Remove sheet index
3065        self.sheet_indexes.remove(&sheet_id);
3066
3067        // If this was the default sheet, pick a new default
3068        if self.default_sheet_id == sheet_id {
3069            // Pick the first available sheet as the new default
3070            if let Some(&new_default) = self.sheet_indexes.keys().next() {
3071                self.default_sheet_id = new_default;
3072            }
3073        }
3074
3075        // Remove the sheet from the registry
3076        self.sheet_reg.remove(sheet_id)?;
3077
3078        // End batch operations
3079        self.end_batch();
3080
3081        Ok(())
3082    }
3083
3084    /// Rename an existing sheet
3085    ///
3086    /// Changes the name of a sheet while preserving all its contents and references.
3087    /// All formulas that reference the sheet by name are automatically updated.
3088    ///
3089    /// # Arguments
3090    /// * `sheet_id` - The ID of the sheet to rename
3091    /// * `new_name` - The new name for the sheet
3092    ///
3093    /// # Errors
3094    /// Returns an error if:
3095    /// - The sheet doesn't exist
3096    /// - The new name is already used by another sheet
3097    /// - The new name is invalid (empty or too long)
3098    pub fn rename_sheet(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
3099        // Validate new name
3100        if new_name.is_empty() || new_name.len() > 255 {
3101            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
3102        }
3103
3104        // Check if sheet exists
3105        let old_name = self.sheet_reg.name(sheet_id);
3106        if old_name.is_empty() {
3107            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Sheet does not exist"));
3108        }
3109
3110        // Check if new name is already taken
3111        if let Some(existing_id) = self.sheet_reg.get_id(new_name) {
3112            if existing_id != sheet_id {
3113                return Err(ExcelError::new(ExcelErrorKind::Value)
3114                    .with_message(format!("Sheet '{new_name}' already exists")));
3115            }
3116            // Same name, nothing to do
3117            return Ok(());
3118        }
3119
3120        // Store old name for formula updates
3121        let old_name = old_name.to_string();
3122
3123        // Update sheet registry
3124        self.sheet_reg.rename(sheet_id, new_name)?;
3125
3126        // Update all formulas that reference the old sheet name
3127        // This requires parsing and updating AST nodes
3128        let formulas_to_update: Vec<VertexId> = self.vertex_formulas.keys().copied().collect();
3129
3130        for formula_id in formulas_to_update {
3131            if let Some(ast) = self.get_formula(formula_id) {
3132                let updated_ast = update_sheet_references_in_ast(&ast, &old_name, new_name);
3133                if ast != updated_ast {
3134                    // Formula changed, update it
3135                    let ast_id = self.data_store.store_ast(&updated_ast, &self.sheet_reg);
3136                    self.vertex_formulas.insert(formula_id, ast_id);
3137                    self.mark_vertex_dirty(formula_id);
3138                }
3139            }
3140        }
3141
3142        Ok(())
3143    }
3144
3145    /// Duplicate an existing sheet
3146    ///
3147    /// Creates a copy of a sheet with all its contents. The new sheet will have
3148    /// the specified name and contain copies of all cells, values, and formulas
3149    /// from the source sheet. Formula references within the sheet are updated
3150    /// to point to the new sheet.
3151    ///
3152    /// # Arguments
3153    /// * `source_sheet_id` - The ID of the sheet to duplicate
3154    /// * `new_name` - The name for the new sheet
3155    ///
3156    /// # Returns
3157    /// The SheetId of the newly created sheet
3158    ///
3159    /// # Errors
3160    /// Returns an error if:
3161    /// - The source sheet doesn't exist
3162    /// - The new name is already used
3163    /// - The new name is invalid
3164    pub fn duplicate_sheet(
3165        &mut self,
3166        source_sheet_id: SheetId,
3167        new_name: &str,
3168    ) -> Result<SheetId, ExcelError> {
3169        // Validate new name
3170        if new_name.is_empty() || new_name.len() > 255 {
3171            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
3172        }
3173
3174        // Check if source sheet exists
3175        let source_name = self.sheet_reg.name(source_sheet_id).to_string();
3176        if source_name.is_empty() {
3177            return Err(
3178                ExcelError::new(ExcelErrorKind::Value).with_message("Source sheet does not exist")
3179            );
3180        }
3181
3182        // Check if new name is already taken
3183        if self.sheet_reg.get_id(new_name).is_some() {
3184            return Err(ExcelError::new(ExcelErrorKind::Value)
3185                .with_message(format!("Sheet '{new_name}' already exists")));
3186        }
3187
3188        // Create the new sheet
3189        let new_sheet_id = self.add_sheet(new_name)?;
3190
3191        // Begin batch operations
3192        self.begin_batch();
3193
3194        // Collect all vertices in the source sheet
3195        let source_vertices: Vec<(VertexId, PackedCoord)> = self
3196            .vertices_in_sheet(source_sheet_id)
3197            .map(|id| (id, self.store.coord(id)))
3198            .collect();
3199
3200        // Map from old vertex IDs to new vertex IDs
3201        let mut vertex_mapping = FxHashMap::default();
3202
3203        // First pass: Create all vertices in the new sheet
3204        for (old_id, coord) in &source_vertices {
3205            let row = coord.row();
3206            let col = coord.col();
3207            let kind = self.store.kind(*old_id);
3208
3209            // Allocate new vertex
3210            let new_id = self.store.allocate(*coord, new_sheet_id, 0x01); // Mark as dirty
3211
3212            // Add to CSR edges
3213            self.edges.add_vertex(*coord, new_id.0);
3214
3215            // Add to sheet index
3216            self.sheet_index_mut(new_sheet_id)
3217                .add_vertex(*coord, new_id);
3218
3219            // Set vertex kind
3220            self.store.set_kind(new_id, kind);
3221
3222            // Copy value or formula
3223            if let Some(&value_ref) = self.vertex_values.get(old_id) {
3224                self.vertex_values.insert(new_id, value_ref);
3225            }
3226
3227            // Store mapping
3228            vertex_mapping.insert(*old_id, new_id);
3229
3230            // Add to cell mapping
3231            let cell_ref = CellRef::new(new_sheet_id, Coord::new(row, col, true, true));
3232            self.cell_to_vertex.insert(cell_ref, new_id);
3233        }
3234
3235        // Second pass: Copy formulas and update references
3236        for (old_id, _) in &source_vertices {
3237            if let Some(&new_id) = vertex_mapping.get(old_id) {
3238                if let Some(&ast_id) = self.vertex_formulas.get(old_id) {
3239                    if let Some(ast) = self.data_store.retrieve_ast(ast_id, &self.sheet_reg) {
3240                        // Update internal sheet references from source to new sheet
3241                        let updated_ast = update_internal_sheet_references(
3242                            &ast,
3243                            &source_name,
3244                            new_name,
3245                            source_sheet_id,
3246                            new_sheet_id,
3247                        );
3248
3249                        // Store updated formula
3250                        let new_ast_id = self.data_store.store_ast(&updated_ast, &self.sheet_reg);
3251                        self.vertex_formulas.insert(new_id, new_ast_id);
3252
3253                        // Extract and add dependencies
3254                        if let Ok((deps, range_deps, _)) =
3255                            self.extract_dependencies(&updated_ast, new_sheet_id)
3256                        {
3257                            // Map dependencies to new sheet where applicable
3258                            let mapped_deps: Vec<VertexId> = deps
3259                                .iter()
3260                                .map(|&dep_id| {
3261                                    // If dependency is in the source sheet, map to new vertex
3262                                    vertex_mapping.get(&dep_id).copied().unwrap_or(dep_id)
3263                                })
3264                                .collect();
3265
3266                            self.add_dependent_edges(new_id, &mapped_deps);
3267                            self.add_range_dependent_edges(new_id, &range_deps, new_sheet_id);
3268                        }
3269                    }
3270                }
3271            }
3272        }
3273
3274        // Copy sheet-scoped named ranges
3275        let sheet_names: Vec<(String, NamedRange)> = self
3276            .sheet_named_ranges
3277            .iter()
3278            .filter(|((sid, _), _)| *sid == source_sheet_id)
3279            .map(|((_, name), range)| (name.clone(), range.clone()))
3280            .collect();
3281
3282        for (name, mut named_range) in sheet_names {
3283            // Update scope to new sheet
3284            named_range.scope = NameScope::Sheet(new_sheet_id);
3285
3286            // Update definition references if needed
3287            match &mut named_range.definition {
3288                NamedDefinition::Cell(cell_ref) if cell_ref.sheet_id == source_sheet_id => {
3289                    cell_ref.sheet_id = new_sheet_id;
3290                }
3291                NamedDefinition::Range(range_ref) => {
3292                    if range_ref.start.sheet_id == source_sheet_id {
3293                        range_ref.start.sheet_id = new_sheet_id;
3294                        range_ref.end.sheet_id = new_sheet_id;
3295                    }
3296                }
3297                _ => {}
3298            }
3299
3300            self.sheet_named_ranges
3301                .insert((new_sheet_id, name), named_range);
3302        }
3303
3304        // End batch operations
3305        self.end_batch();
3306
3307        Ok(new_sheet_id)
3308    }
3309}
3310
3311/// Helper function to update sheet references in an AST
3312fn update_sheet_references_in_ast(ast: &ASTNode, old_name: &str, new_name: &str) -> ASTNode {
3313    match &ast.node_type {
3314        ASTNodeType::Reference { reference, .. } => {
3315            let updated_ref = match reference {
3316                ReferenceType::Cell { sheet, row, col } => {
3317                    if sheet.as_deref() == Some(old_name) {
3318                        ReferenceType::Cell {
3319                            sheet: Some(new_name.to_string()),
3320                            row: *row,
3321                            col: *col,
3322                        }
3323                    } else {
3324                        reference.clone()
3325                    }
3326                }
3327                ReferenceType::Range {
3328                    sheet,
3329                    start_row,
3330                    start_col,
3331                    end_row,
3332                    end_col,
3333                } => {
3334                    if sheet.as_deref() == Some(old_name) {
3335                        ReferenceType::Range {
3336                            sheet: Some(new_name.to_string()),
3337                            start_row: *start_row,
3338                            start_col: *start_col,
3339                            end_row: *end_row,
3340                            end_col: *end_col,
3341                        }
3342                    } else {
3343                        reference.clone()
3344                    }
3345                }
3346                _ => reference.clone(),
3347            };
3348
3349            ASTNode {
3350                node_type: ASTNodeType::Reference {
3351                    original: String::new(),
3352                    reference: updated_ref,
3353                },
3354                source_token: None,
3355                contains_volatile: ast.contains_volatile,
3356            }
3357        }
3358        ASTNodeType::BinaryOp { op, left, right } => ASTNode {
3359            node_type: ASTNodeType::BinaryOp {
3360                op: op.clone(),
3361                left: Box::new(update_sheet_references_in_ast(left, old_name, new_name)),
3362                right: Box::new(update_sheet_references_in_ast(right, old_name, new_name)),
3363            },
3364            source_token: None,
3365            contains_volatile: ast.contains_volatile,
3366        },
3367        ASTNodeType::UnaryOp { op, expr } => ASTNode {
3368            node_type: ASTNodeType::UnaryOp {
3369                op: op.clone(),
3370                expr: Box::new(update_sheet_references_in_ast(expr, old_name, new_name)),
3371            },
3372            source_token: None,
3373            contains_volatile: ast.contains_volatile,
3374        },
3375        ASTNodeType::Function { name, args } => ASTNode {
3376            node_type: ASTNodeType::Function {
3377                name: name.clone(),
3378                args: args
3379                    .iter()
3380                    .map(|arg| update_sheet_references_in_ast(arg, old_name, new_name))
3381                    .collect(),
3382            },
3383            source_token: None,
3384            contains_volatile: ast.contains_volatile,
3385        },
3386        ASTNodeType::Array(rows) => ASTNode {
3387            node_type: ASTNodeType::Array(
3388                rows.iter()
3389                    .map(|row| {
3390                        row.iter()
3391                            .map(|cell| update_sheet_references_in_ast(cell, old_name, new_name))
3392                            .collect()
3393                    })
3394                    .collect(),
3395            ),
3396            source_token: None,
3397            contains_volatile: ast.contains_volatile,
3398        },
3399        _ => ast.clone(),
3400    }
3401}
3402
3403/// Helper function to update internal sheet references when duplicating a sheet
3404fn update_internal_sheet_references(
3405    ast: &ASTNode,
3406    source_name: &str,
3407    new_name: &str,
3408    source_id: SheetId,
3409    new_id: SheetId,
3410) -> ASTNode {
3411    match &ast.node_type {
3412        ASTNodeType::Reference { reference, .. } => {
3413            let updated_ref = match reference {
3414                ReferenceType::Cell { sheet, row, col } => {
3415                    // Update references without sheet name (internal) or with source sheet name
3416                    if sheet.is_none() || sheet.as_deref() == Some(source_name) {
3417                        ReferenceType::Cell {
3418                            sheet: Some(new_name.to_string()),
3419                            row: *row,
3420                            col: *col,
3421                        }
3422                    } else {
3423                        reference.clone()
3424                    }
3425                }
3426                ReferenceType::Range {
3427                    sheet,
3428                    start_row,
3429                    start_col,
3430                    end_row,
3431                    end_col,
3432                } => {
3433                    if sheet.is_none() || sheet.as_deref() == Some(source_name) {
3434                        ReferenceType::Range {
3435                            sheet: Some(new_name.to_string()),
3436                            start_row: *start_row,
3437                            start_col: *start_col,
3438                            end_row: *end_row,
3439                            end_col: *end_col,
3440                        }
3441                    } else {
3442                        reference.clone()
3443                    }
3444                }
3445                _ => reference.clone(),
3446            };
3447
3448            ASTNode {
3449                node_type: ASTNodeType::Reference {
3450                    original: String::new(),
3451                    reference: updated_ref,
3452                },
3453                source_token: None,
3454                contains_volatile: ast.contains_volatile,
3455            }
3456        }
3457        ASTNodeType::BinaryOp { op, left, right } => ASTNode {
3458            node_type: ASTNodeType::BinaryOp {
3459                op: op.clone(),
3460                left: Box::new(update_internal_sheet_references(
3461                    left,
3462                    source_name,
3463                    new_name,
3464                    source_id,
3465                    new_id,
3466                )),
3467                right: Box::new(update_internal_sheet_references(
3468                    right,
3469                    source_name,
3470                    new_name,
3471                    source_id,
3472                    new_id,
3473                )),
3474            },
3475            source_token: None,
3476            contains_volatile: ast.contains_volatile,
3477        },
3478        ASTNodeType::UnaryOp { op, expr } => ASTNode {
3479            node_type: ASTNodeType::UnaryOp {
3480                op: op.clone(),
3481                expr: Box::new(update_internal_sheet_references(
3482                    expr,
3483                    source_name,
3484                    new_name,
3485                    source_id,
3486                    new_id,
3487                )),
3488            },
3489            source_token: None,
3490            contains_volatile: ast.contains_volatile,
3491        },
3492        ASTNodeType::Function { name, args } => ASTNode {
3493            node_type: ASTNodeType::Function {
3494                name: name.clone(),
3495                args: args
3496                    .iter()
3497                    .map(|arg| {
3498                        update_internal_sheet_references(
3499                            arg,
3500                            source_name,
3501                            new_name,
3502                            source_id,
3503                            new_id,
3504                        )
3505                    })
3506                    .collect(),
3507            },
3508            source_token: None,
3509            contains_volatile: ast.contains_volatile,
3510        },
3511        ASTNodeType::Array(rows) => ASTNode {
3512            node_type: ASTNodeType::Array(
3513                rows.iter()
3514                    .map(|row| {
3515                        row.iter()
3516                            .map(|cell| {
3517                                update_internal_sheet_references(
3518                                    cell,
3519                                    source_name,
3520                                    new_name,
3521                                    source_id,
3522                                    new_id,
3523                                )
3524                            })
3525                            .collect()
3526                    })
3527                    .collect(),
3528            ),
3529            source_token: None,
3530            contains_volatile: ast.contains_volatile,
3531        },
3532        _ => ast.clone(),
3533    }
3534}
3535
3536/// Helper function to adjust a named definition during structural operations
3537fn adjust_named_definition(
3538    definition: &mut NamedDefinition,
3539    adjuster: &crate::engine::graph::editor::reference_adjuster::ReferenceAdjuster,
3540    operation: &crate::engine::graph::editor::reference_adjuster::ShiftOperation,
3541) -> Result<(), ExcelError> {
3542    match definition {
3543        NamedDefinition::Cell(cell_ref) => {
3544            if let Some(adjusted) = adjuster.adjust_cell_ref(cell_ref, operation) {
3545                *cell_ref = adjusted;
3546            } else {
3547                // Cell was deleted, convert to #REF! error
3548                return Err(ExcelError::new(ExcelErrorKind::Ref));
3549            }
3550        }
3551        NamedDefinition::Range(range_ref) => {
3552            let adjusted_start = adjuster.adjust_cell_ref(&range_ref.start, operation);
3553            let adjusted_end = adjuster.adjust_cell_ref(&range_ref.end, operation);
3554
3555            if let (Some(start), Some(end)) = (adjusted_start, adjusted_end) {
3556                range_ref.start = start;
3557                range_ref.end = end;
3558            } else {
3559                return Err(ExcelError::new(ExcelErrorKind::Ref));
3560            }
3561        }
3562        NamedDefinition::Formula {
3563            ast,
3564            dependencies,
3565            range_deps,
3566        } => {
3567            // Adjust AST references
3568            let adjusted_ast = adjuster.adjust_ast(ast, operation);
3569            *ast = adjusted_ast;
3570
3571            // Dependencies will be recalculated on next use
3572            dependencies.clear();
3573            range_deps.clear();
3574        }
3575    }
3576    Ok(())
3577}