Skip to main content

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, 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
16mod ast_utils;
17pub mod editor;
18mod formula_analysis;
19mod names;
20mod range_deps;
21mod sheets;
22pub mod snapshot;
23mod sources;
24mod tables;
25
26use super::arena::{AstNodeId, DataStore, ValueRef};
27use super::delta_edges::CsrMutableEdges;
28use super::sheet_index::SheetIndex;
29use super::vertex::{VertexId, VertexKind};
30use super::vertex_store::{FIRST_NORMAL_VERTEX, VertexStore};
31use crate::engine::topo::{
32    GraphAdapter,
33    pk::{DynamicTopo, PkConfig},
34};
35use crate::reference::{CellRef, Coord, SharedRangeRef, SharedRef, SharedSheetLocator};
36use formualizer_common::Coord as AbsCoord;
37// topo::pk wiring will be integrated behind config.use_dynamic_topo in a follow-up step
38
39pub use editor::change_log::{ChangeEvent, ChangeLog};
40
41// ChangeEvent is now imported from change_log module
42
43/// đź”® Scalability Hook: Dependency reference types for range compression
44#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub enum DependencyRef {
46    /// A specific cell dependency
47    Cell(VertexId),
48    /// A dependency on a finite, rectangular range
49    Range {
50        sheet: String,
51        start_row: u32,
52        start_col: u32,
53        end_row: u32, // Inclusive
54        end_col: u32, // Inclusive
55    },
56    /// A whole column dependency (A:A) - future range compression
57    WholeColumn { sheet: String, col: u32 },
58    /// A whole row dependency (1:1) - future range compression  
59    WholeRow { sheet: String, row: u32 },
60}
61
62/// A key representing a coarse-grained section of a sheet
63#[derive(Debug, Clone, Hash, PartialEq, Eq)]
64pub struct StripeKey {
65    pub sheet_id: SheetId,
66    pub stripe_type: StripeType,
67    pub index: u32, // The index of the row, column, or block stripe
68}
69
70#[derive(Debug, Clone, Hash, PartialEq, Eq)]
71pub enum StripeType {
72    Row,
73    Column,
74    Block, // For dense, square-like ranges
75}
76
77/// Block stripe indexing mathematics
78const BLOCK_H: u32 = 256;
79const BLOCK_W: u32 = 256;
80
81pub fn block_index(row: u32, col: u32) -> u32 {
82    (row / BLOCK_H) << 16 | (col / BLOCK_W)
83}
84
85/// A summary of the results of a mutating operation on the graph.
86/// This serves as a "changelog" to the application layer.
87#[derive(Debug, Clone)]
88pub struct OperationSummary {
89    /// Vertices whose values have been directly or indirectly affected.
90    pub affected_vertices: Vec<VertexId>,
91    /// Placeholder cells that were newly created to satisfy dependencies.
92    pub created_placeholders: Vec<CellRef>,
93}
94
95/// SoA-based dependency graph implementation
96#[derive(Debug)]
97pub struct DependencyGraph {
98    // Core columnar storage
99    store: VertexStore,
100
101    // Edge storage with delta slab
102    edges: CsrMutableEdges,
103
104    // Arena-based value and formula storage
105    data_store: DataStore,
106    vertex_values: FxHashMap<VertexId, ValueRef>,
107    vertex_formulas: FxHashMap<VertexId, AstNodeId>,
108
109    // Address mappings using fast hashing
110    cell_to_vertex: FxHashMap<CellRef, VertexId>,
111
112    // Scheduling state - using HashSet for O(1) operations
113    dirty_vertices: FxHashSet<VertexId>,
114    volatile_vertices: FxHashSet<VertexId>,
115
116    // NEW: Specialized managers for range dependencies (Hybrid Model)
117    /// Maps a formula vertex to the ranges it depends on.
118    formula_to_range_deps: FxHashMap<VertexId, Vec<SharedRangeRef<'static>>>,
119
120    /// Maps a stripe to formulas that depend on it via a compressed range.
121    /// CRITICAL: VertexIds are deduplicated within each stripe to avoid quadratic blow-ups.
122    stripe_to_dependents: FxHashMap<StripeKey, FxHashSet<VertexId>>,
123
124    // Sheet-level sparse indexes for O(log n + k) range queries
125    /// Maps sheet_id to its interval tree index for efficient row/column operations
126    sheet_indexes: FxHashMap<SheetId, SheetIndex>,
127
128    // Sheet name/ID mapping
129    sheet_reg: SheetRegistry,
130    default_sheet_id: SheetId,
131
132    // Named ranges support
133    /// Workbook-scoped named ranges
134    named_ranges: FxHashMap<String, NamedRange>,
135
136    /// Sheet-scoped named ranges  
137    sheet_named_ranges: FxHashMap<(SheetId, String), NamedRange>,
138
139    /// Reverse mapping: vertex -> names it uses (by vertex id)
140    vertex_to_names: FxHashMap<VertexId, Vec<VertexId>>,
141
142    /// Lookup for name vertex -> (scope, name) to avoid map scans
143    name_vertex_lookup: FxHashMap<VertexId, (NameScope, String)>,
144
145    /// Pending formula vertices referencing names not yet defined
146    pending_name_links: FxHashMap<String, Vec<(SheetId, VertexId)>>,
147
148    // Native workbook tables (ListObjects)
149    tables: FxHashMap<String, tables::TableEntry>,
150    table_vertex_lookup: FxHashMap<VertexId, String>,
151
152    // External sources (SourceVertex)
153    source_scalars: FxHashMap<String, sources::SourceScalarEntry>,
154    source_tables: FxHashMap<String, sources::SourceTableEntry>,
155    source_vertex_lookup: FxHashMap<VertexId, String>,
156
157    /// Monotonic counter to assign synthetic coordinates to name vertices
158    name_vertex_seq: u32,
159
160    /// Monotonic counter to assign synthetic coordinates to source vertices
161    source_vertex_seq: u32,
162
163    /// Mapping from cell vertices to named range vertices that depend on them
164    cell_to_name_dependents: FxHashMap<VertexId, FxHashSet<VertexId>>,
165    /// Cached list of cell dependencies per named range vertex (for teardown)
166    name_to_cell_dependencies: FxHashMap<VertexId, Vec<VertexId>>,
167
168    // Evaluation configuration
169    config: super::EvalConfig,
170
171    // Dynamic topology orderer (Pearce–Kelly) maintained alongside edges when enabled
172    pk_order: Option<DynamicTopo<VertexId>>,
173
174    // Spill registry: anchor -> cells, and reverse mapping for blockers
175    spill_anchor_to_cells: FxHashMap<VertexId, Vec<CellRef>>,
176    spill_cell_to_anchor: FxHashMap<CellRef, VertexId>,
177
178    // Hint: during initial bulk load, many cells are guaranteed new; allow skipping existence checks per-sheet
179    first_load_assume_new: bool,
180    ensure_touched_sheets: FxHashSet<SheetId>,
181
182    #[cfg(test)]
183    instr: GraphInstrumentation,
184}
185
186impl Default for DependencyGraph {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192impl DependencyGraph {
193    /// Expose range expansion limit for planners
194    pub fn range_expansion_limit(&self) -> usize {
195        self.config.range_expansion_limit
196    }
197
198    pub fn get_config(&self) -> &super::EvalConfig {
199        &self.config
200    }
201
202    /// Build a dependency plan for a set of formulas on sheets
203    pub fn plan_dependencies<'a, I>(
204        &mut self,
205        items: I,
206        policy: &formualizer_parse::parser::CollectPolicy,
207        volatile: Option<&[bool]>,
208    ) -> Result<crate::engine::plan::DependencyPlan, formualizer_common::ExcelError>
209    where
210        I: IntoIterator<Item = (&'a str, u32, u32, &'a formualizer_parse::parser::ASTNode)>,
211    {
212        crate::engine::plan::build_dependency_plan(
213            &mut self.sheet_reg,
214            items.into_iter(),
215            policy,
216            volatile,
217        )
218    }
219
220    /// Ensure vertices exist for given coords; allocate missing in contiguous batches and add to edges/index.
221    /// Returns a list suitable for edges.add_vertices_batch.
222    pub fn ensure_vertices_batch(
223        &mut self,
224        coords: &[(SheetId, AbsCoord)],
225    ) -> Vec<(AbsCoord, u32)> {
226        use rustc_hash::FxHashMap;
227        let mut grouped: FxHashMap<SheetId, Vec<AbsCoord>> = FxHashMap::default();
228        for (sid, pc) in coords.iter().copied() {
229            let addr = CellRef::new(sid, Coord::new(pc.row(), pc.col(), true, true));
230            if self.cell_to_vertex.contains_key(&addr) {
231                continue;
232            }
233            grouped.entry(sid).or_default().push(pc);
234        }
235        let mut add_batch: Vec<(AbsCoord, u32)> = Vec::new();
236        for (sid, pcs) in grouped {
237            if pcs.is_empty() {
238                continue;
239            }
240            // Mark sheet as touched by ensure to disable fast-path new allocations for its values
241            self.ensure_touched_sheets.insert(sid);
242            let vids = self.store.allocate_contiguous(sid, &pcs, 0x00);
243            for (i, pc) in pcs.iter().enumerate() {
244                let vid = vids[i];
245                add_batch.push((*pc, vid.0));
246                let addr = CellRef::new(sid, Coord::new(pc.row(), pc.col(), true, true));
247                self.cell_to_vertex.insert(addr, vid);
248                // Respect sheet index mode: skip index updates in Lazy mode during bulk ensure
249                match self.config.sheet_index_mode {
250                    crate::engine::SheetIndexMode::Eager
251                    | crate::engine::SheetIndexMode::FastBatch => {
252                        self.sheet_index_mut(sid).add_vertex(*pc, vid);
253                    }
254                    crate::engine::SheetIndexMode::Lazy => {
255                        // defer index build
256                    }
257                }
258            }
259        }
260        if !add_batch.is_empty() {
261            self.edges.add_vertices_batch(&add_batch);
262        }
263        add_batch
264    }
265
266    /// Enable/disable the first-load fast path for value inserts.
267    pub fn set_first_load_assume_new(&mut self, enabled: bool) {
268        self.first_load_assume_new = enabled;
269    }
270
271    /// Reset the per-sheet ensure touch tracking.
272    pub fn reset_ensure_touched(&mut self) {
273        self.ensure_touched_sheets.clear();
274    }
275
276    /// Store ASTs in batch and return their arena ids
277    pub fn store_asts_batch<'a, I>(&mut self, asts: I) -> Vec<AstNodeId>
278    where
279        I: IntoIterator<Item = &'a formualizer_parse::parser::ASTNode>,
280    {
281        self.data_store.store_asts_batch(asts, &self.sheet_reg)
282    }
283
284    /// Lookup VertexId for a (SheetId, AbsCoord)
285    pub fn vid_for_sid_pc(&self, sid: SheetId, pc: AbsCoord) -> Option<VertexId> {
286        let addr = CellRef::new(sid, Coord::new(pc.row(), pc.col(), true, true));
287        self.cell_to_vertex.get(&addr).copied()
288    }
289
290    /// Helper to map a global cell index in a plan to a VertexId
291    pub fn vid_for_plan_idx(
292        &self,
293        plan: &crate::engine::plan::DependencyPlan,
294        idx: u32,
295    ) -> Option<VertexId> {
296        let (sid, pc) = plan.global_cells.get(idx as usize).copied()?;
297        self.vid_for_sid_pc(sid, pc)
298    }
299
300    /// Assign a formula to an existing vertex, removing prior edges and setting flags
301    pub fn assign_formula_vertex(&mut self, vid: VertexId, ast_id: AstNodeId, volatile: bool) {
302        if self.vertex_formulas.contains_key(&vid) {
303            self.remove_dependent_edges(vid);
304        }
305        self.store
306            .set_kind(vid, crate::engine::vertex::VertexKind::FormulaScalar);
307        self.vertex_values.remove(&vid);
308        self.vertex_formulas.insert(vid, ast_id);
309        self.mark_volatile(vid, volatile);
310        // schedule evaluation
311        self.mark_vertex_dirty(vid);
312    }
313
314    /// Public wrapper for adding edges without beginning a batch (caller manages batch)
315    pub fn add_edges_nobatch(&mut self, dependent: VertexId, dependencies: &[VertexId]) {
316        self.add_dependent_edges_nobatch(dependent, dependencies);
317    }
318
319    /// Iterate all normal vertex ids
320    pub fn iter_vertex_ids(&self) -> impl Iterator<Item = VertexId> + '_ {
321        self.store.all_vertices()
322    }
323
324    /// Get current AbsCoord for a vertex
325    pub fn vertex_coord(&self, vid: VertexId) -> AbsCoord {
326        self.store.coord(vid)
327    }
328
329    /// Total number of allocated vertices (including deleted)
330    pub fn vertex_count(&self) -> usize {
331        self.store.len()
332    }
333
334    /// Replace CSR edges in one shot from adjacency and coords
335    pub fn build_edges_from_adjacency(
336        &mut self,
337        adjacency: Vec<(u32, Vec<u32>)>,
338        coords: Vec<AbsCoord>,
339        vertex_ids: Vec<u32>,
340    ) {
341        self.edges
342            .build_from_adjacency(adjacency, coords, vertex_ids);
343    }
344    /// Compute min/max used row among vertices within [start_col..=end_col] on a sheet.
345    pub fn used_row_bounds_for_columns(
346        &self,
347        sheet_id: SheetId,
348        start_col: u32,
349        end_col: u32,
350    ) -> Option<(u32, u32)> {
351        // Prefer sheet index when available
352        if let Some(index) = self.sheet_indexes.get(&sheet_id)
353            && !index.is_empty()
354        {
355            let mut min_r: Option<u32> = None;
356            let mut max_r: Option<u32> = None;
357            for vid in index.vertices_in_col_range(start_col, end_col) {
358                let r = self.store.coord(vid).row();
359                min_r = Some(min_r.map(|m| m.min(r)).unwrap_or(r));
360                max_r = Some(max_r.map(|m| m.max(r)).unwrap_or(r));
361            }
362            return match (min_r, max_r) {
363                (Some(a), Some(b)) => Some((a, b)),
364                _ => None,
365            };
366        }
367        // Fallback: scan cell map for bounds on the fly
368        let mut min_r: Option<u32> = None;
369        let mut max_r: Option<u32> = None;
370        for cref in self.cell_to_vertex.keys() {
371            if cref.sheet_id == sheet_id {
372                let c = cref.coord.col();
373                if c >= start_col && c <= end_col {
374                    let r = cref.coord.row();
375                    min_r = Some(min_r.map(|m| m.min(r)).unwrap_or(r));
376                    max_r = Some(max_r.map(|m| m.max(r)).unwrap_or(r));
377                }
378            }
379        }
380        match (min_r, max_r) {
381            (Some(a), Some(b)) => Some((a, b)),
382            _ => None,
383        }
384    }
385
386    /// Build (or rebuild) the sheet index for a given sheet if running in Lazy mode.
387    pub fn finalize_sheet_index(&mut self, sheet: &str) {
388        let Some(sheet_id) = self.sheet_reg.get_id(sheet) else {
389            return;
390        };
391        // If already present and non-empty, skip
392        if let Some(idx) = self.sheet_indexes.get(&sheet_id)
393            && !idx.is_empty()
394        {
395            return;
396        }
397        let mut idx = SheetIndex::new();
398        // Collect coords for this sheet
399        let mut batch: Vec<(AbsCoord, VertexId)> = Vec::with_capacity(self.cell_to_vertex.len());
400        for (cref, vid) in &self.cell_to_vertex {
401            if cref.sheet_id == sheet_id {
402                batch.push((AbsCoord::new(cref.coord.row(), cref.coord.col()), *vid));
403            }
404        }
405        // Use batch builder
406        idx.add_vertices_batch(&batch);
407        self.sheet_indexes.insert(sheet_id, idx);
408    }
409
410    pub fn set_sheet_index_mode(&mut self, mode: crate::engine::SheetIndexMode) {
411        self.config.sheet_index_mode = mode;
412    }
413
414    /// Compute min/max used column among vertices within [start_row..=end_row] on a sheet.
415    pub fn used_col_bounds_for_rows(
416        &self,
417        sheet_id: SheetId,
418        start_row: u32,
419        end_row: u32,
420    ) -> Option<(u32, u32)> {
421        if let Some(index) = self.sheet_indexes.get(&sheet_id)
422            && !index.is_empty()
423        {
424            let mut min_c: Option<u32> = None;
425            let mut max_c: Option<u32> = None;
426            for vid in index.vertices_in_row_range(start_row, end_row) {
427                let c = self.store.coord(vid).col();
428                min_c = Some(min_c.map(|m| m.min(c)).unwrap_or(c));
429                max_c = Some(max_c.map(|m| m.max(c)).unwrap_or(c));
430            }
431            return match (min_c, max_c) {
432                (Some(a), Some(b)) => Some((a, b)),
433                _ => None,
434            };
435        }
436        // Fallback: scan cell map on the fly
437        let mut min_c: Option<u32> = None;
438        let mut max_c: Option<u32> = None;
439        for cref in self.cell_to_vertex.keys() {
440            if cref.sheet_id == sheet_id {
441                let r = cref.coord.row();
442                if r >= start_row && r <= end_row {
443                    let c = cref.coord.col();
444                    min_c = Some(min_c.map(|m| m.min(c)).unwrap_or(c));
445                    max_c = Some(max_c.map(|m| m.max(c)).unwrap_or(c));
446                }
447            }
448        }
449        match (min_c, max_c) {
450            (Some(a), Some(b)) => Some((a, b)),
451            _ => None,
452        }
453    }
454
455    /// Returns true if the given sheet currently contains any formula vertices.
456    pub fn sheet_has_formulas(&self, sheet_id: SheetId) -> bool {
457        // Check vertex_formulas keys; they represent formula vertices
458        for &vid in self.vertex_formulas.keys() {
459            if self.store.sheet_id(vid) == sheet_id {
460                return true;
461            }
462        }
463        false
464    }
465    pub fn new() -> Self {
466        Self::new_with_config(super::EvalConfig::default())
467    }
468
469    pub fn new_with_config(config: super::EvalConfig) -> Self {
470        let mut sheet_reg = SheetRegistry::new();
471        let default_sheet_id = sheet_reg.id_for(&config.default_sheet_name);
472
473        let mut g = Self {
474            store: VertexStore::new(),
475            edges: CsrMutableEdges::new(),
476            data_store: DataStore::new(),
477            vertex_values: FxHashMap::default(),
478            vertex_formulas: FxHashMap::default(),
479            cell_to_vertex: FxHashMap::default(),
480            dirty_vertices: FxHashSet::default(),
481            volatile_vertices: FxHashSet::default(),
482            formula_to_range_deps: FxHashMap::default(),
483            stripe_to_dependents: FxHashMap::default(),
484            sheet_indexes: FxHashMap::default(),
485            sheet_reg,
486            default_sheet_id,
487            named_ranges: FxHashMap::default(),
488            sheet_named_ranges: FxHashMap::default(),
489            vertex_to_names: FxHashMap::default(),
490            name_vertex_lookup: FxHashMap::default(),
491            pending_name_links: FxHashMap::default(),
492            tables: FxHashMap::default(),
493            table_vertex_lookup: FxHashMap::default(),
494            source_scalars: FxHashMap::default(),
495            source_tables: FxHashMap::default(),
496            source_vertex_lookup: FxHashMap::default(),
497            name_vertex_seq: 0,
498            source_vertex_seq: 0,
499            cell_to_name_dependents: FxHashMap::default(),
500            name_to_cell_dependencies: FxHashMap::default(),
501            config: config.clone(),
502            pk_order: None,
503            spill_anchor_to_cells: FxHashMap::default(),
504            spill_cell_to_anchor: FxHashMap::default(),
505            first_load_assume_new: false,
506            ensure_touched_sheets: FxHashSet::default(),
507            #[cfg(test)]
508            instr: GraphInstrumentation::default(),
509        };
510
511        if config.use_dynamic_topo {
512            // Seed with currently active vertices (likely empty at startup)
513            let nodes = g
514                .store
515                .all_vertices()
516                .filter(|&id| g.store.vertex_exists_active(id));
517            let mut pk = DynamicTopo::new(
518                nodes,
519                PkConfig {
520                    visit_budget: config.pk_visit_budget,
521                    compaction_interval_ops: config.pk_compaction_interval_ops,
522                },
523            );
524            // Build an initial order using current graph
525            let adapter = GraphAdapter { g: &g };
526            pk.rebuild_full(&adapter);
527            g.pk_order = Some(pk);
528        }
529
530        g
531    }
532
533    /// When dynamic topology is enabled, compute layers for a subset using PK ordering.
534    pub(crate) fn pk_layers_for(&self, subset: &[VertexId]) -> Option<Vec<crate::engine::Layer>> {
535        let pk = self.pk_order.as_ref()?;
536        let adapter = crate::engine::topo::GraphAdapter { g: self };
537        let layers = pk.layers_for(&adapter, subset, self.config.max_layer_width);
538        Some(
539            layers
540                .into_iter()
541                .map(|vs| crate::engine::Layer { vertices: vs })
542                .collect(),
543        )
544    }
545
546    #[inline]
547    pub(crate) fn dynamic_topo_enabled(&self) -> bool {
548        self.pk_order.is_some()
549    }
550
551    #[cfg(test)]
552    pub fn reset_instr(&mut self) {
553        self.instr = GraphInstrumentation::default();
554    }
555
556    #[cfg(test)]
557    pub fn instr(&self) -> GraphInstrumentation {
558        self.instr.clone()
559    }
560
561    /// Begin batch operations - defer CSR rebuilds until end_batch() is called
562    pub fn begin_batch(&mut self) {
563        self.edges.begin_batch();
564    }
565
566    /// End batch operations and trigger CSR rebuild if needed
567    pub fn end_batch(&mut self) {
568        self.edges.end_batch();
569    }
570
571    pub fn default_sheet_id(&self) -> SheetId {
572        self.default_sheet_id
573    }
574
575    pub fn default_sheet_name(&self) -> &str {
576        self.sheet_reg.name(self.default_sheet_id)
577    }
578
579    pub fn set_default_sheet_by_name(&mut self, name: &str) {
580        self.default_sheet_id = self.sheet_id_mut(name);
581    }
582
583    pub fn set_default_sheet_by_id(&mut self, id: SheetId) {
584        self.default_sheet_id = id;
585    }
586
587    /// Returns the ID for a sheet name, creating one if it doesn't exist.
588    pub fn sheet_id_mut(&mut self, name: &str) -> SheetId {
589        self.sheet_reg.id_for(name)
590    }
591
592    pub fn sheet_id(&self, name: &str) -> Option<SheetId> {
593        self.sheet_reg.get_id(name)
594    }
595
596    /// Resolve a sheet name to an existing ID or return a #REF! error.
597    fn resolve_existing_sheet_id(&self, name: &str) -> Result<SheetId, ExcelError> {
598        self.sheet_id(name).ok_or_else(|| {
599            ExcelError::new(ExcelErrorKind::Ref).with_message(format!("Sheet not found: {name}"))
600        })
601    }
602
603    /// Returns the name of a sheet given its ID.
604    pub fn sheet_name(&self, id: SheetId) -> &str {
605        self.sheet_reg.name(id)
606    }
607
608    /// Access the sheet registry (read-only) for external bindings
609    pub fn sheet_reg(&self) -> &SheetRegistry {
610        &self.sheet_reg
611    }
612
613    pub(crate) fn data_store(&self) -> &DataStore {
614        &self.data_store
615    }
616
617    /// Converts a `CellRef` to a fully qualified A1-style string (e.g., "SheetName!A1").
618    pub fn to_a1(&self, cell_ref: CellRef) -> String {
619        format!("{}!{}", self.sheet_name(cell_ref.sheet_id), cell_ref.coord)
620    }
621
622    pub(crate) fn vertex_len(&self) -> usize {
623        self.store.len()
624    }
625
626    /// Get mutable access to a sheet's index, creating it if it doesn't exist
627    /// This is the primary way VertexEditor and internal operations access the index
628    pub fn sheet_index_mut(&mut self, sheet_id: SheetId) -> &mut SheetIndex {
629        self.sheet_indexes.entry(sheet_id).or_default()
630    }
631
632    /// Get immutable access to a sheet's index, returns None if not initialized
633    pub fn sheet_index(&self, sheet_id: SheetId) -> Option<&SheetIndex> {
634        self.sheet_indexes.get(&sheet_id)
635    }
636
637    /// Set a value in a cell, returns affected vertex IDs
638    pub fn set_cell_value(
639        &mut self,
640        sheet: &str,
641        row: u32,
642        col: u32,
643        value: LiteralValue,
644    ) -> Result<OperationSummary, ExcelError> {
645        let sheet_id = self.sheet_id_mut(sheet);
646        // External API is 1-based; store 0-based coords internally.
647        let coord = Coord::from_excel(row, col, true, true);
648        let addr = CellRef::new(sheet_id, coord);
649        let mut created_placeholders = Vec::new();
650
651        let vertex_id = if let Some(&existing_id) = self.cell_to_vertex.get(&addr) {
652            // Check if it was a formula and remove dependencies
653            let is_formula = matches!(
654                self.store.kind(existing_id),
655                VertexKind::FormulaScalar | VertexKind::FormulaArray
656            );
657
658            if is_formula {
659                self.remove_dependent_edges(existing_id);
660                self.vertex_formulas.remove(&existing_id);
661            }
662
663            // Update to value kind
664            self.store.set_kind(existing_id, VertexKind::Cell);
665            let value_ref = self.data_store.store_value(value);
666            self.vertex_values.insert(existing_id, value_ref);
667            existing_id
668        } else {
669            // Create new vertex
670            created_placeholders.push(addr);
671            let packed_coord = AbsCoord::from_excel(row, col);
672            let vertex_id = self.store.allocate(packed_coord, sheet_id, 0x01); // dirty flag
673
674            // Add vertex coordinate for CSR
675            self.edges.add_vertex(packed_coord, vertex_id.0);
676
677            // Add to sheet index for O(log n + k) range queries
678            self.sheet_index_mut(sheet_id)
679                .add_vertex(packed_coord, vertex_id);
680
681            self.store.set_kind(vertex_id, VertexKind::Cell);
682            let value_ref = self.data_store.store_value(value);
683            self.vertex_values.insert(vertex_id, value_ref);
684            self.cell_to_vertex.insert(addr, vertex_id);
685            vertex_id
686        };
687
688        Ok(OperationSummary {
689            affected_vertices: self.mark_dirty(vertex_id),
690            created_placeholders,
691        })
692    }
693
694    /// Reserve capacity hints for upcoming bulk cell inserts (values only for now).
695    pub fn reserve_cells(&mut self, additional: usize) {
696        self.store.reserve(additional);
697        self.vertex_values.reserve(additional);
698        self.cell_to_vertex.reserve(additional);
699        // sheet_indexes: cannot easily reserve per-sheet without distribution; skip.
700    }
701
702    /// Fast path for initial bulk load of value cells: avoids dirty propagation & dependency work.
703    pub fn set_cell_value_bulk_untracked(
704        &mut self,
705        sheet: &str,
706        row: u32,
707        col: u32,
708        value: LiteralValue,
709    ) {
710        let sheet_id = self.sheet_id_mut(sheet);
711        let coord = Coord::from_excel(row, col, true, true);
712        let addr = CellRef::new(sheet_id, coord);
713        if let Some(&existing_id) = self.cell_to_vertex.get(&addr) {
714            // Overwrite existing value vertex only (ignore formulas in bulk path)
715            let value_ref = self.data_store.store_value(value);
716            self.vertex_values.insert(existing_id, value_ref);
717            self.store.set_kind(existing_id, VertexKind::Cell);
718            return;
719        }
720        let packed_coord = AbsCoord::from_excel(row, col);
721        let vertex_id = self.store.allocate(packed_coord, sheet_id, 0x00); // not dirty
722        self.edges.add_vertex(packed_coord, vertex_id.0);
723        self.sheet_index_mut(sheet_id)
724            .add_vertex(packed_coord, vertex_id);
725        self.store.set_kind(vertex_id, VertexKind::Cell);
726        let value_ref = self.data_store.store_value(value);
727        self.vertex_values.insert(vertex_id, value_ref);
728        self.cell_to_vertex.insert(addr, vertex_id);
729    }
730
731    /// Bulk insert a collection of plain value cells (no formulas) more efficiently.
732    pub fn bulk_insert_values<I>(&mut self, sheet: &str, cells: I)
733    where
734        I: IntoIterator<Item = (u32, u32, LiteralValue)>,
735    {
736        use web_time::Instant;
737        let t0 = Instant::now();
738        // Collect first to know size
739        let collected: Vec<(u32, u32, LiteralValue)> = cells.into_iter().collect();
740        if collected.is_empty() {
741            return;
742        }
743        let sheet_id = self.sheet_id_mut(sheet);
744        self.reserve_cells(collected.len());
745        let t_reserve = Instant::now();
746        let mut new_vertices: Vec<(AbsCoord, u32)> = Vec::with_capacity(collected.len());
747        let mut index_items: Vec<(AbsCoord, VertexId)> = Vec::with_capacity(collected.len());
748        // For new allocations, accumulate values and assign after a single batch store
749        let mut new_value_coords: Vec<(AbsCoord, VertexId)> = Vec::with_capacity(collected.len());
750        let mut new_value_literals: Vec<LiteralValue> = Vec::with_capacity(collected.len());
751        // Detect fast path: during initial ingest, caller may guarantee most cells are new.
752        let assume_new = self.first_load_assume_new
753            && self
754                .sheet_id(sheet)
755                .map(|sid| !self.ensure_touched_sheets.contains(&sid))
756                .unwrap_or(false);
757
758        for (row, col, value) in collected {
759            let coord = Coord::from_excel(row, col, true, true);
760            let addr = CellRef::new(sheet_id, coord);
761            if !assume_new && let Some(&existing_id) = self.cell_to_vertex.get(&addr) {
762                let value_ref = self.data_store.store_value(value);
763                self.vertex_values.insert(existing_id, value_ref);
764                self.store.set_kind(existing_id, VertexKind::Cell);
765                continue;
766            }
767            let packed = AbsCoord::from_excel(row, col);
768            let vertex_id = self.store.allocate(packed, sheet_id, 0x00);
769            self.store.set_kind(vertex_id, VertexKind::Cell);
770            // Defer value arena storage to a single batch
771            new_value_coords.push((packed, vertex_id));
772            new_value_literals.push(value);
773            self.cell_to_vertex.insert(addr, vertex_id);
774            new_vertices.push((packed, vertex_id.0));
775            index_items.push((packed, vertex_id));
776        }
777        // Perform a single batch store for newly allocated values
778        if !new_value_literals.is_empty() {
779            let vrefs = self.data_store.store_values_batch(new_value_literals);
780            debug_assert_eq!(vrefs.len(), new_value_coords.len());
781            for (i, (_pc, vid)) in new_value_coords.iter().enumerate() {
782                self.vertex_values.insert(*vid, vrefs[i]);
783            }
784        }
785        let t_after_alloc = Instant::now();
786        if !new_vertices.is_empty() {
787            let t_edges_start = Instant::now();
788            self.edges.add_vertices_batch(&new_vertices);
789            let t_edges_done = Instant::now();
790
791            match self.config.sheet_index_mode {
792                crate::engine::SheetIndexMode::Eager => {
793                    self.sheet_index_mut(sheet_id)
794                        .add_vertices_batch(&index_items);
795                }
796                crate::engine::SheetIndexMode::Lazy => {
797                    // Skip building index now; will be built on-demand
798                }
799                crate::engine::SheetIndexMode::FastBatch => {
800                    // FastBatch for now delegates to same batch insert (future: build from sorted arrays)
801                    self.sheet_index_mut(sheet_id)
802                        .add_vertices_batch(&index_items);
803                }
804            }
805            let t_index_done = Instant::now();
806        }
807    }
808
809    /// Set a formula in a cell, returns affected vertex IDs
810    pub fn set_cell_formula(
811        &mut self,
812        sheet: &str,
813        row: u32,
814        col: u32,
815        ast: ASTNode,
816    ) -> Result<OperationSummary, ExcelError> {
817        let volatile = self.is_ast_volatile(&ast);
818        self.set_cell_formula_with_volatility(sheet, row, col, ast, volatile)
819    }
820
821    /// Set a formula in a cell with a known volatility flag (context-scoped detection upstream)
822    pub fn set_cell_formula_with_volatility(
823        &mut self,
824        sheet: &str,
825        row: u32,
826        col: u32,
827        ast: ASTNode,
828        volatile: bool,
829    ) -> Result<OperationSummary, ExcelError> {
830        let dbg = std::env::var("FZ_DEBUG_LOAD")
831            .ok()
832            .is_some_and(|v| v != "0");
833        let dep_ms_thresh: u128 = std::env::var("FZ_DEBUG_DEP_MS")
834            .ok()
835            .and_then(|s| s.parse().ok())
836            .unwrap_or(0);
837        let sample_n: usize = std::env::var("FZ_DEBUG_SAMPLE_N")
838            .ok()
839            .and_then(|s| s.parse().ok())
840            .unwrap_or(0);
841        let t0 = if dbg {
842            Some(web_time::Instant::now())
843        } else {
844            None
845        };
846        let sheet_id = self.sheet_id_mut(sheet);
847        let coord = Coord::from_excel(row, col, true, true);
848        let addr = CellRef::new(sheet_id, coord);
849
850        // Extract dependencies from AST, creating placeholders if needed
851        let t_dep0 = if dbg {
852            Some(web_time::Instant::now())
853        } else {
854            None
855        };
856        let (
857            new_dependencies,
858            new_range_dependencies,
859            mut created_placeholders,
860            named_dependencies,
861        ) = self.extract_dependencies(&ast, sheet_id)?;
862        if let (true, Some(t)) = (dbg, t_dep0) {
863            let elapsed = t.elapsed().as_millis();
864            // Only log if over threshold or sampled
865            let do_log = (dep_ms_thresh > 0 && elapsed >= dep_ms_thresh)
866                || (sample_n > 0 && (row as usize).is_multiple_of(sample_n));
867            if dep_ms_thresh == 0 && sample_n == 0 {
868                // default: very light sampling every 1000 rows
869                if row.is_multiple_of(1000) {
870                    eprintln!(
871                        "[fz][dep] {}!{} extracted: deps={}, ranges={}, placeholders={}, names={} in {} ms",
872                        self.sheet_name(sheet_id),
873                        crate::reference::Coord::from_excel(row, col, true, true),
874                        new_dependencies.len(),
875                        new_range_dependencies.len(),
876                        created_placeholders.len(),
877                        named_dependencies.len(),
878                        elapsed
879                    );
880                }
881            } else if do_log {
882                eprintln!(
883                    "[fz][dep] {}!{} extracted: deps={}, ranges={}, placeholders={}, names={} in {} ms",
884                    self.sheet_name(sheet_id),
885                    crate::reference::Coord::from_excel(row, col, true, true),
886                    new_dependencies.len(),
887                    new_range_dependencies.len(),
888                    created_placeholders.len(),
889                    named_dependencies.len(),
890                    elapsed
891                );
892            }
893        }
894
895        // Check for self-reference (immediate cycle detection)
896        let addr_vertex_id = self.get_or_create_vertex(&addr, &mut created_placeholders);
897
898        if new_dependencies.contains(&addr_vertex_id) {
899            return Err(ExcelError::new(ExcelErrorKind::Circ)
900                .with_message("Self-reference detected".to_string()));
901        }
902
903        for &name_vertex in &named_dependencies {
904            let mut visited = FxHashSet::default();
905            if self.name_depends_on_vertex(name_vertex, addr_vertex_id, &mut visited) {
906                return Err(ExcelError::new(ExcelErrorKind::Circ)
907                    .with_message("Circular reference through named range".to_string()));
908            }
909        }
910
911        // Remove old dependencies first
912        self.remove_dependent_edges(addr_vertex_id);
913        self.detach_vertex_from_names(addr_vertex_id);
914
915        // Update vertex properties
916        self.store
917            .set_kind(addr_vertex_id, VertexKind::FormulaScalar);
918        let ast_id = self.data_store.store_ast(&ast, &self.sheet_reg);
919        self.vertex_formulas.insert(addr_vertex_id, ast_id);
920        self.store.set_dirty(addr_vertex_id, true);
921
922        // Clear any cached value since this is now a formula
923        self.vertex_values.remove(&addr_vertex_id);
924
925        self.mark_volatile(addr_vertex_id, volatile);
926
927        if !named_dependencies.is_empty() {
928            self.attach_vertex_to_names(addr_vertex_id, &named_dependencies);
929        }
930
931        if let (true, Some(t)) = (dbg, t0) {
932            let elapsed = t.elapsed().as_millis();
933            let log_set = dep_ms_thresh > 0 && elapsed >= dep_ms_thresh;
934            if log_set {
935                eprintln!(
936                    "[fz][set] {}!{} total {} ms",
937                    self.sheet_name(sheet_id),
938                    crate::reference::Coord::from_excel(row, col, true, true),
939                    elapsed
940                );
941            }
942        }
943
944        // Add new dependency edges
945        self.add_dependent_edges(addr_vertex_id, &new_dependencies);
946        self.add_range_dependent_edges(addr_vertex_id, &new_range_dependencies, sheet_id);
947
948        Ok(OperationSummary {
949            affected_vertices: self.mark_dirty(addr_vertex_id),
950            created_placeholders,
951        })
952    }
953
954    pub fn set_cell_value_ref(
955        &mut self,
956        cell: formualizer_common::SheetCellRef<'_>,
957        value: LiteralValue,
958    ) -> Result<OperationSummary, ExcelError> {
959        let owned = cell.into_owned();
960        let sheet_id = match owned.sheet {
961            formualizer_common::SheetLocator::Id(id) => id,
962            formualizer_common::SheetLocator::Name(name) => self.sheet_id_mut(name.as_ref()),
963            formualizer_common::SheetLocator::Current => self.default_sheet_id,
964        };
965        let sheet_name = self.sheet_name(sheet_id).to_string();
966        self.set_cell_value(
967            &sheet_name,
968            owned.coord.row() + 1,
969            owned.coord.col() + 1,
970            value,
971        )
972    }
973
974    pub fn set_cell_formula_ref(
975        &mut self,
976        cell: formualizer_common::SheetCellRef<'_>,
977        ast: ASTNode,
978    ) -> Result<OperationSummary, ExcelError> {
979        let owned = cell.into_owned();
980        let sheet_id = match owned.sheet {
981            formualizer_common::SheetLocator::Id(id) => id,
982            formualizer_common::SheetLocator::Name(name) => self.sheet_id_mut(name.as_ref()),
983            formualizer_common::SheetLocator::Current => self.default_sheet_id,
984        };
985        let sheet_name = self.sheet_name(sheet_id).to_string();
986        self.set_cell_formula(
987            &sheet_name,
988            owned.coord.row() + 1,
989            owned.coord.col() + 1,
990            ast,
991        )
992    }
993
994    pub fn get_cell_value_ref(
995        &self,
996        cell: formualizer_common::SheetCellRef<'_>,
997    ) -> Option<LiteralValue> {
998        let owned = cell.into_owned();
999        let sheet_id = match owned.sheet {
1000            formualizer_common::SheetLocator::Id(id) => id,
1001            formualizer_common::SheetLocator::Name(name) => self.sheet_id(name.as_ref())?,
1002            formualizer_common::SheetLocator::Current => self.default_sheet_id,
1003        };
1004        let sheet_name = self.sheet_name(sheet_id);
1005        self.get_cell_value(sheet_name, owned.coord.row() + 1, owned.coord.col() + 1)
1006    }
1007
1008    /// Get current value from a cell
1009    pub fn get_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
1010        let sheet_id = self.sheet_reg.get_id(sheet)?;
1011        let coord = Coord::from_excel(row, col, true, true);
1012        let addr = CellRef::new(sheet_id, coord);
1013
1014        self.cell_to_vertex.get(&addr).and_then(|&vertex_id| {
1015            // Check values hashmap (stores both cell values and formula results)
1016            self.vertex_values
1017                .get(&vertex_id)
1018                .map(|&value_ref| self.data_store.retrieve_value(value_ref))
1019        })
1020    }
1021
1022    /// Mark vertex dirty and propagate to dependents
1023    fn mark_dirty(&mut self, vertex_id: VertexId) -> Vec<VertexId> {
1024        let mut affected = FxHashSet::default();
1025        let mut to_visit = Vec::new();
1026        let mut visited_for_propagation = FxHashSet::default();
1027
1028        // Only mark the source vertex as dirty if it's a formula
1029        // Value cells don't get marked dirty themselves but are still affected
1030        let is_formula = matches!(
1031            self.store.kind(vertex_id),
1032            VertexKind::FormulaScalar
1033                | VertexKind::FormulaArray
1034                | VertexKind::NamedScalar
1035                | VertexKind::NamedArray
1036        );
1037
1038        if is_formula {
1039            to_visit.push(vertex_id);
1040        } else {
1041            // Value cells are affected (for tracking) but not marked dirty
1042            affected.insert(vertex_id);
1043        }
1044
1045        // Initial propagation from direct and range dependents
1046        {
1047            // Get dependents (vertices that depend on this vertex)
1048            let dependents = self.get_dependents(vertex_id);
1049            to_visit.extend(&dependents);
1050
1051            if let Some(name_set) = self.cell_to_name_dependents.get(&vertex_id) {
1052                for &name_vertex in name_set {
1053                    to_visit.push(name_vertex);
1054                }
1055            }
1056
1057            // Check range dependencies
1058            let view = self.store.view(vertex_id);
1059            let row = view.row();
1060            let col = view.col();
1061            let dirty_sheet_id = view.sheet_id();
1062
1063            // New stripe-based dependents lookup
1064            let mut potential_dependents = FxHashSet::default();
1065
1066            // 1. Column stripe lookup
1067            let column_key = StripeKey {
1068                sheet_id: dirty_sheet_id,
1069                stripe_type: StripeType::Column,
1070                index: col,
1071            };
1072            if let Some(dependents) = self.stripe_to_dependents.get(&column_key) {
1073                potential_dependents.extend(dependents);
1074            }
1075
1076            // 2. Row stripe lookup
1077            let row_key = StripeKey {
1078                sheet_id: dirty_sheet_id,
1079                stripe_type: StripeType::Row,
1080                index: row,
1081            };
1082            if let Some(dependents) = self.stripe_to_dependents.get(&row_key) {
1083                potential_dependents.extend(dependents);
1084            }
1085
1086            // 3. Block stripe lookup
1087            if self.config.enable_block_stripes {
1088                let block_key = StripeKey {
1089                    sheet_id: dirty_sheet_id,
1090                    stripe_type: StripeType::Block,
1091                    index: block_index(row, col),
1092                };
1093                if let Some(dependents) = self.stripe_to_dependents.get(&block_key) {
1094                    potential_dependents.extend(dependents);
1095                }
1096            }
1097
1098            // Precision check: ensure the dirtied cell is actually within the formula's range
1099            for &dep_id in &potential_dependents {
1100                if let Some(ranges) = self.formula_to_range_deps.get(&dep_id) {
1101                    for range in ranges {
1102                        let range_sheet_id = match range.sheet {
1103                            SharedSheetLocator::Id(id) => id,
1104                            _ => dirty_sheet_id,
1105                        };
1106                        if range_sheet_id != dirty_sheet_id {
1107                            continue;
1108                        }
1109                        let sr0 = range.start_row.map(|b| b.index).unwrap_or(0);
1110                        let er0 = range.end_row.map(|b| b.index).unwrap_or(u32::MAX);
1111                        let sc0 = range.start_col.map(|b| b.index).unwrap_or(0);
1112                        let ec0 = range.end_col.map(|b| b.index).unwrap_or(u32::MAX);
1113                        if row >= sr0 && row <= er0 && col >= sc0 && col <= ec0 {
1114                            to_visit.push(dep_id);
1115                            break;
1116                        }
1117                    }
1118                }
1119            }
1120        }
1121
1122        while let Some(id) = to_visit.pop() {
1123            if !visited_for_propagation.insert(id) {
1124                continue; // Already processed
1125            }
1126            affected.insert(id);
1127
1128            // Mark vertex as dirty
1129            self.store.set_dirty(id, true);
1130
1131            // Add direct dependents to visit list
1132            let dependents = self.get_dependents(id);
1133            to_visit.extend(&dependents);
1134        }
1135
1136        // Add to dirty set
1137        self.dirty_vertices.extend(&affected);
1138
1139        // Return as Vec for compatibility
1140        affected.into_iter().collect()
1141    }
1142
1143    /// Get all vertices that need evaluation
1144    pub fn get_evaluation_vertices(&self) -> Vec<VertexId> {
1145        let mut combined = FxHashSet::default();
1146        combined.extend(&self.dirty_vertices);
1147        combined.extend(&self.volatile_vertices);
1148
1149        let mut result: Vec<VertexId> = combined
1150            .into_iter()
1151            .filter(|&id| {
1152                // Only include formula vertices
1153                matches!(
1154                    self.store.kind(id),
1155                    VertexKind::FormulaScalar
1156                        | VertexKind::FormulaArray
1157                        | VertexKind::NamedScalar
1158                        | VertexKind::NamedArray
1159                )
1160            })
1161            .collect();
1162        result.sort_unstable();
1163        result
1164    }
1165
1166    /// Clear dirty flags after successful evaluation
1167    pub fn clear_dirty_flags(&mut self, vertices: &[VertexId]) {
1168        for &vertex_id in vertices {
1169            self.store.set_dirty(vertex_id, false);
1170            self.dirty_vertices.remove(&vertex_id);
1171        }
1172    }
1173
1174    /// đź”® Scalability Hook: Clear volatile vertices after evaluation cycle
1175    pub fn clear_volatile_flags(&mut self) {
1176        self.volatile_vertices.clear();
1177    }
1178
1179    /// Re-marks all volatile vertices as dirty for the next evaluation cycle.
1180    pub(crate) fn redirty_volatiles(&mut self) {
1181        let volatile_ids: Vec<VertexId> = self.volatile_vertices.iter().copied().collect();
1182        for id in volatile_ids {
1183            self.mark_dirty(id);
1184        }
1185    }
1186
1187    fn get_or_create_vertex(
1188        &mut self,
1189        addr: &CellRef,
1190        created_placeholders: &mut Vec<CellRef>,
1191    ) -> VertexId {
1192        if let Some(&vertex_id) = self.cell_to_vertex.get(addr) {
1193            return vertex_id;
1194        }
1195
1196        created_placeholders.push(*addr);
1197        let packed_coord = AbsCoord::new(addr.coord.row(), addr.coord.col());
1198        let vertex_id = self.store.allocate(packed_coord, addr.sheet_id, 0x00);
1199
1200        // Add vertex coordinate for CSR
1201        self.edges.add_vertex(packed_coord, vertex_id.0);
1202
1203        // Add to sheet index for O(log n + k) range queries
1204        self.sheet_index_mut(addr.sheet_id)
1205            .add_vertex(packed_coord, vertex_id);
1206
1207        self.store.set_kind(vertex_id, VertexKind::Empty);
1208        self.cell_to_vertex.insert(*addr, vertex_id);
1209        vertex_id
1210    }
1211
1212    fn add_dependent_edges(&mut self, dependent: VertexId, dependencies: &[VertexId]) {
1213        // Batch to avoid repeated CSR rebuilds and keep reverse edges current
1214        self.edges.begin_batch();
1215
1216        // If PK enabled, update order using a short-lived adapter without holding &mut self
1217        // Track dependencies that should be skipped if rejecting cycle-creating edges
1218        let mut skip_deps: rustc_hash::FxHashSet<VertexId> = rustc_hash::FxHashSet::default();
1219        if self.pk_order.is_some()
1220            && let Some(mut pk) = self.pk_order.take()
1221        {
1222            pk.ensure_nodes(std::iter::once(dependent));
1223            pk.ensure_nodes(dependencies.iter().copied());
1224            {
1225                let adapter = GraphAdapter { g: self };
1226                for &dep_id in dependencies {
1227                    match pk.try_add_edge(&adapter, dep_id, dependent) {
1228                        Ok(_) => {}
1229                        Err(_cycle) => {
1230                            if self.config.pk_reject_cycle_edges {
1231                                skip_deps.insert(dep_id);
1232                            } else {
1233                                pk.rebuild_full(&adapter);
1234                            }
1235                        }
1236                    }
1237                }
1238            } // drop adapter
1239            self.pk_order = Some(pk);
1240        }
1241
1242        // Now mutate engine edges; if rejecting cycles, re-check and skip those that would create cycles
1243        for &dep_id in dependencies {
1244            if self.config.pk_reject_cycle_edges && skip_deps.contains(&dep_id) {
1245                continue;
1246            }
1247            self.edges.add_edge(dependent, dep_id);
1248            #[cfg(test)]
1249            {
1250                self.instr.edges_added += 1;
1251            }
1252        }
1253
1254        self.edges.end_batch();
1255    }
1256
1257    /// Like add_dependent_edges, but assumes caller is managing edges.begin_batch/end_batch
1258    fn add_dependent_edges_nobatch(&mut self, dependent: VertexId, dependencies: &[VertexId]) {
1259        // If PK enabled, update order using a short-lived adapter without holding &mut self
1260        let mut skip_deps: rustc_hash::FxHashSet<VertexId> = rustc_hash::FxHashSet::default();
1261        if self.pk_order.is_some()
1262            && let Some(mut pk) = self.pk_order.take()
1263        {
1264            pk.ensure_nodes(std::iter::once(dependent));
1265            pk.ensure_nodes(dependencies.iter().copied());
1266            {
1267                let adapter = GraphAdapter { g: self };
1268                for &dep_id in dependencies {
1269                    match pk.try_add_edge(&adapter, dep_id, dependent) {
1270                        Ok(_) => {}
1271                        Err(_cycle) => {
1272                            if self.config.pk_reject_cycle_edges {
1273                                skip_deps.insert(dep_id);
1274                            } else {
1275                                pk.rebuild_full(&adapter);
1276                            }
1277                        }
1278                    }
1279                }
1280            }
1281            self.pk_order = Some(pk);
1282        }
1283
1284        for &dep_id in dependencies {
1285            if self.config.pk_reject_cycle_edges && skip_deps.contains(&dep_id) {
1286                continue;
1287            }
1288            self.edges.add_edge(dependent, dep_id);
1289            #[cfg(test)]
1290            {
1291                self.instr.edges_added += 1;
1292            }
1293        }
1294    }
1295
1296    /// Bulk set formulas on a sheet using a single dependency plan and batched edge updates.
1297    pub fn bulk_set_formulas<I>(&mut self, sheet: &str, items: I) -> Result<usize, ExcelError>
1298    where
1299        I: IntoIterator<Item = (u32, u32, ASTNode)>,
1300    {
1301        let collected: Vec<(u32, u32, ASTNode)> = items.into_iter().collect();
1302        if collected.is_empty() {
1303            return Ok(0);
1304        }
1305        let vol_flags: Vec<bool> = collected
1306            .iter()
1307            .map(|(_, _, ast)| self.is_ast_volatile(ast))
1308            .collect();
1309        self.bulk_set_formulas_with_volatility(sheet, collected, vol_flags)
1310    }
1311
1312    pub fn bulk_set_formulas_with_volatility(
1313        &mut self,
1314        sheet: &str,
1315        collected: Vec<(u32, u32, ASTNode)>,
1316        vol_flags: Vec<bool>,
1317    ) -> Result<usize, ExcelError> {
1318        use formualizer_parse::parser::CollectPolicy;
1319        let sheet_id = self.sheet_id_mut(sheet);
1320
1321        if collected.is_empty() {
1322            return Ok(0);
1323        }
1324
1325        // 1) Build plan across all formulas (read-only, no graph mutation)
1326        let tiny_refs = collected.iter().map(|(r, c, ast)| (sheet, *r, *c, ast));
1327        let policy = CollectPolicy {
1328            expand_small_ranges: true,
1329            range_expansion_limit: self.config.range_expansion_limit,
1330            include_names: true,
1331        };
1332        let plan = crate::engine::plan::build_dependency_plan(
1333            &mut self.sheet_reg,
1334            tiny_refs,
1335            &policy,
1336            Some(&vol_flags),
1337        )?;
1338
1339        // 2) Ensure/create target vertices and referenced cells (placeholders) once
1340        let mut created_placeholders: Vec<CellRef> = Vec::new();
1341
1342        // Targets
1343        let mut target_vids: Vec<VertexId> = Vec::with_capacity(plan.formula_targets.len());
1344        for (sid, pc) in &plan.formula_targets {
1345            let addr = CellRef::new(*sid, Coord::new(pc.row(), pc.col(), true, true));
1346            let vid = if let Some(&existing) = self.cell_to_vertex.get(&addr) {
1347                existing
1348            } else {
1349                self.get_or_create_vertex(&addr, &mut created_placeholders)
1350            };
1351            target_vids.push(vid);
1352        }
1353
1354        // Global referenced cells
1355        let mut dep_vids: Vec<VertexId> = Vec::with_capacity(plan.global_cells.len());
1356        for (sid, pc) in &plan.global_cells {
1357            let addr = CellRef::new(*sid, Coord::new(pc.row(), pc.col(), true, true));
1358            let vid = if let Some(&existing) = self.cell_to_vertex.get(&addr) {
1359                existing
1360            } else {
1361                self.get_or_create_vertex(&addr, &mut created_placeholders)
1362            };
1363            dep_vids.push(vid);
1364        }
1365
1366        // 3) Store ASTs in batch and update kinds/flags/value map
1367        let ast_ids = self
1368            .data_store
1369            .store_asts_batch(collected.iter().map(|(_, _, ast)| ast), &self.sheet_reg);
1370
1371        for (i, &tvid) in target_vids.iter().enumerate() {
1372            // If this cell already had a formula, remove its edges once here
1373            if self.vertex_formulas.contains_key(&tvid) {
1374                self.remove_dependent_edges(tvid);
1375            }
1376            self.store.set_kind(tvid, VertexKind::FormulaScalar);
1377            self.store.set_dirty(tvid, true);
1378            self.vertex_values.remove(&tvid);
1379            self.vertex_formulas.insert(tvid, ast_ids[i]);
1380            self.mark_volatile(tvid, vol_flags.get(i).copied().unwrap_or(false));
1381        }
1382
1383        // 4) Add edges in one batch
1384        self.edges.begin_batch();
1385        for (i, tvid) in target_vids.iter().copied().enumerate() {
1386            let mut deps: Vec<VertexId> = Vec::new();
1387
1388            // Map per-formula indices into dep_vids
1389            if let Some(indices) = plan.per_formula_cells.get(i) {
1390                deps.reserve(indices.len());
1391                for &idx in indices {
1392                    if let Some(vid) = dep_vids.get(idx as usize) {
1393                        deps.push(*vid);
1394                    }
1395                }
1396            }
1397
1398            if let Some(names) = plan.per_formula_names.get(i)
1399                && !names.is_empty()
1400            {
1401                let mut name_vertices = Vec::new();
1402                let formula_sheet = plan
1403                    .formula_targets
1404                    .get(i)
1405                    .map(|(sid, _)| *sid)
1406                    .unwrap_or(sheet_id);
1407                for name in names {
1408                    if let Some(named) = self.resolve_name_entry(name, formula_sheet) {
1409                        deps.push(named.vertex);
1410                        name_vertices.push(named.vertex);
1411                    } else if let Some(source) = self.resolve_source_scalar_entry(name) {
1412                        deps.push(source.vertex);
1413                    } else {
1414                        self.record_pending_name_reference(formula_sheet, name, tvid);
1415                    }
1416                }
1417                if !name_vertices.is_empty() {
1418                    self.attach_vertex_to_names(tvid, &name_vertices);
1419                }
1420            }
1421
1422            if let Some(tables) = plan.per_formula_tables.get(i)
1423                && !tables.is_empty()
1424            {
1425                for table_name in tables {
1426                    if let Some(table) = self.resolve_table_entry(table_name) {
1427                        deps.push(table.vertex);
1428                    } else if let Some(source) = self.resolve_source_table_entry(table_name) {
1429                        deps.push(source.vertex);
1430                    }
1431                }
1432            }
1433
1434            if !deps.is_empty() {
1435                self.add_dependent_edges_nobatch(tvid, &deps);
1436            }
1437
1438            // Range deps from plan are already compact RangeKeys; register directly.
1439            if let Some(rks) = plan.per_formula_ranges.get(i) {
1440                self.add_range_deps_from_keys(tvid, rks, sheet_id);
1441            }
1442        }
1443        self.edges.end_batch();
1444
1445        Ok(collected.len())
1446    }
1447
1448    /// Public (crate) helper to add a single dependency edge (dependent -> dependency) used for restoration/undo.
1449    pub fn add_dependency_edge(&mut self, dependent: VertexId, dependency: VertexId) {
1450        if dependent == dependency {
1451            return;
1452        }
1453        // If PK enabled attempt to add maintaining ordering; fallback to rebuild if cycle
1454        if self.pk_order.is_some()
1455            && let Some(mut pk) = self.pk_order.take()
1456        {
1457            pk.ensure_nodes(std::iter::once(dependent));
1458            pk.ensure_nodes(std::iter::once(dependency));
1459            let adapter = GraphAdapter { g: self };
1460            if pk.try_add_edge(&adapter, dependency, dependent).is_err() {
1461                // Cycle: rebuild full (conservative)
1462                pk.rebuild_full(&adapter);
1463            }
1464            self.pk_order = Some(pk);
1465        }
1466        self.edges.add_edge(dependent, dependency);
1467        self.store.set_dirty(dependent, true);
1468        self.dirty_vertices.insert(dependent);
1469    }
1470
1471    fn remove_dependent_edges(&mut self, vertex: VertexId) {
1472        // Remove all outgoing edges from this vertex (its dependencies)
1473        let dependencies = self.edges.out_edges(vertex);
1474
1475        self.edges.begin_batch();
1476        if self.pk_order.is_some()
1477            && let Some(mut pk) = self.pk_order.take()
1478        {
1479            for dep in &dependencies {
1480                pk.remove_edge(*dep, vertex);
1481            }
1482            self.pk_order = Some(pk);
1483        }
1484        for dep in dependencies {
1485            self.edges.remove_edge(vertex, dep);
1486        }
1487        self.edges.end_batch();
1488
1489        // Remove range dependencies and clean up stripes
1490        if let Some(old_ranges) = self.formula_to_range_deps.remove(&vertex) {
1491            let old_sheet_id = self.store.sheet_id(vertex);
1492
1493            for range in &old_ranges {
1494                let sheet_id = match range.sheet {
1495                    SharedSheetLocator::Id(id) => id,
1496                    _ => old_sheet_id,
1497                };
1498                let s_row = range.start_row.map(|b| b.index);
1499                let e_row = range.end_row.map(|b| b.index);
1500                let s_col = range.start_col.map(|b| b.index);
1501                let e_col = range.end_col.map(|b| b.index);
1502
1503                let mut keys_to_clean = FxHashSet::default();
1504
1505                let col_stripes = (s_row.is_none() && e_row.is_none())
1506                    || (s_col.is_some() && e_col.is_some() && (s_row.is_none() || e_row.is_none()));
1507                let row_stripes = (s_col.is_none() && e_col.is_none())
1508                    || (s_row.is_some() && e_row.is_some() && (s_col.is_none() || e_col.is_none()));
1509
1510                if col_stripes && !row_stripes {
1511                    let sc = s_col.unwrap_or(0);
1512                    let ec = e_col.unwrap_or(sc);
1513                    for col in sc..=ec {
1514                        keys_to_clean.insert(StripeKey {
1515                            sheet_id,
1516                            stripe_type: StripeType::Column,
1517                            index: col,
1518                        });
1519                    }
1520                } else if row_stripes && !col_stripes {
1521                    let sr = s_row.unwrap_or(0);
1522                    let er = e_row.unwrap_or(sr);
1523                    for row in sr..=er {
1524                        keys_to_clean.insert(StripeKey {
1525                            sheet_id,
1526                            stripe_type: StripeType::Row,
1527                            index: row,
1528                        });
1529                    }
1530                } else {
1531                    let start_row = s_row.unwrap_or(0);
1532                    let start_col = s_col.unwrap_or(0);
1533                    let end_row = e_row.unwrap_or(start_row);
1534                    let end_col = e_col.unwrap_or(start_col);
1535
1536                    let height = end_row.saturating_sub(start_row) + 1;
1537                    let width = end_col.saturating_sub(start_col) + 1;
1538
1539                    if self.config.enable_block_stripes && height > 1 && width > 1 {
1540                        let start_block_row = start_row / BLOCK_H;
1541                        let end_block_row = end_row / BLOCK_H;
1542                        let start_block_col = start_col / BLOCK_W;
1543                        let end_block_col = end_col / BLOCK_W;
1544
1545                        for block_row in start_block_row..=end_block_row {
1546                            for block_col in start_block_col..=end_block_col {
1547                                keys_to_clean.insert(StripeKey {
1548                                    sheet_id,
1549                                    stripe_type: StripeType::Block,
1550                                    index: block_index(block_row * BLOCK_H, block_col * BLOCK_W),
1551                                });
1552                            }
1553                        }
1554                    } else if height > width {
1555                        for col in start_col..=end_col {
1556                            keys_to_clean.insert(StripeKey {
1557                                sheet_id,
1558                                stripe_type: StripeType::Column,
1559                                index: col,
1560                            });
1561                        }
1562                    } else {
1563                        for row in start_row..=end_row {
1564                            keys_to_clean.insert(StripeKey {
1565                                sheet_id,
1566                                stripe_type: StripeType::Row,
1567                                index: row,
1568                            });
1569                        }
1570                    }
1571                }
1572
1573                for key in keys_to_clean {
1574                    if let Some(dependents) = self.stripe_to_dependents.get_mut(&key) {
1575                        dependents.remove(&vertex);
1576                        if dependents.is_empty() {
1577                            self.stripe_to_dependents.remove(&key);
1578                            #[cfg(test)]
1579                            {
1580                                self.instr.stripe_removes += 1;
1581                            }
1582                        }
1583                    }
1584                }
1585            }
1586        }
1587    }
1588
1589    // Removed: vertices() and get_vertex() methods - no longer needed with SoA
1590    // The old AoS Vertex struct has been eliminated in favor of direct
1591    // access to columnar data through the VertexStore
1592
1593    /// Updates the cached value of a formula vertex.
1594    pub(crate) fn update_vertex_value(&mut self, vertex_id: VertexId, value: LiteralValue) {
1595        let value_ref = self.data_store.store_value(value);
1596        self.vertex_values.insert(vertex_id, value_ref);
1597    }
1598
1599    /// Plan a spill region for an anchor; returns #SPILL! if blocked
1600    pub fn plan_spill_region(
1601        &self,
1602        anchor: VertexId,
1603        target_cells: &[CellRef],
1604    ) -> Result<(), ExcelError> {
1605        use formualizer_common::{ExcelErrorExtra, ExcelErrorKind};
1606        // Compute expected spill shape from the target rectangle for better diagnostics
1607        let (expected_rows, expected_cols) = if target_cells.is_empty() {
1608            (0u32, 0u32)
1609        } else {
1610            let mut min_r = u32::MAX;
1611            let mut max_r = 0u32;
1612            let mut min_c = u32::MAX;
1613            let mut max_c = 0u32;
1614            for cell in target_cells {
1615                let r = cell.coord.row();
1616                let c = cell.coord.col();
1617                if r < min_r {
1618                    min_r = r;
1619                }
1620                if r > max_r {
1621                    max_r = r;
1622                }
1623                if c < min_c {
1624                    min_c = c;
1625                }
1626                if c > max_c {
1627                    max_c = c;
1628                }
1629            }
1630            (
1631                max_r.saturating_sub(min_r).saturating_add(1),
1632                max_c.saturating_sub(min_c).saturating_add(1),
1633            )
1634        };
1635        // Allow overlapping with previously owned spill cells by this anchor
1636        for cell in target_cells {
1637            // If cell is already owned by this anchor's previous spill, it's allowed.
1638            let owned_by_anchor = match self.spill_cell_to_anchor.get(cell) {
1639                Some(&existing_anchor) if existing_anchor == anchor => true,
1640                Some(_other) => {
1641                    return Err(ExcelError::new(ExcelErrorKind::Spill)
1642                        .with_message("BlockedBySpill")
1643                        .with_extra(ExcelErrorExtra::Spill {
1644                            expected_rows,
1645                            expected_cols,
1646                        }));
1647                }
1648                None => false,
1649            };
1650
1651            if owned_by_anchor {
1652                continue;
1653            }
1654
1655            // If cell is occupied by another formula anchor, block regardless of value visibility
1656            if let Some(&vid) = self.cell_to_vertex.get(cell)
1657                && vid != anchor
1658            {
1659                // Prevent clobbering formulas (array or scalar) in the target area
1660                match self.store.kind(vid) {
1661                    VertexKind::FormulaScalar | VertexKind::FormulaArray => {
1662                        return Err(ExcelError::new(ExcelErrorKind::Spill)
1663                            .with_message("BlockedByFormula")
1664                            .with_extra(ExcelErrorExtra::Spill {
1665                                expected_rows,
1666                                expected_cols,
1667                            }));
1668                    }
1669                    _ => {
1670                        // If a non-empty value exists (and not this anchor), block
1671                        if let Some(vref) = self.vertex_values.get(&vid) {
1672                            let v = self.data_store.retrieve_value(*vref);
1673                            if !matches!(v, LiteralValue::Empty) {
1674                                return Err(ExcelError::new(ExcelErrorKind::Spill)
1675                                    .with_message("BlockedByValue")
1676                                    .with_extra(ExcelErrorExtra::Spill {
1677                                        expected_rows,
1678                                        expected_cols,
1679                                    }));
1680                            }
1681                        }
1682                    }
1683                }
1684            }
1685        }
1686        Ok(())
1687    }
1688
1689    // Note: non-atomic commit_spill_region has been removed. All callers must use
1690    // commit_spill_region_atomic_with_fault for atomicity and rollback on failure.
1691
1692    /// Commit a spill atomically with an internal shadow buffer and optional fault injection.
1693    /// If a fault is injected partway through, all changes are rolled back to the pre-commit state.
1694    /// This does not change behavior under normal operation; it's primarily for Phase 3 guarantees and tests.
1695    pub fn commit_spill_region_atomic_with_fault(
1696        &mut self,
1697        anchor: VertexId,
1698        target_cells: Vec<CellRef>,
1699        values: Vec<Vec<LiteralValue>>,
1700        fault_after_ops: Option<usize>,
1701    ) -> Result<(), ExcelError> {
1702        use rustc_hash::FxHashSet;
1703
1704        // Capture previous owned cells for this anchor
1705        let prev_cells = self
1706            .spill_anchor_to_cells
1707            .get(&anchor)
1708            .cloned()
1709            .unwrap_or_default();
1710        let new_set: FxHashSet<CellRef> = target_cells.iter().copied().collect();
1711        let prev_set: FxHashSet<CellRef> = prev_cells.iter().copied().collect();
1712
1713        // Compose operation list: clears first (prev - new), then writes for new rectangle
1714        #[derive(Clone)]
1715        struct Op {
1716            sheet: String,
1717            row: u32,
1718            col: u32,
1719            new_value: LiteralValue,
1720        }
1721        let mut ops: Vec<Op> = Vec::new();
1722
1723        // Clears for cells no longer used
1724        for cell in prev_cells.iter() {
1725            if !new_set.contains(cell) {
1726                let sheet = self.sheet_name(cell.sheet_id).to_string();
1727                ops.push(Op {
1728                    sheet,
1729                    row: cell.coord.row(),
1730                    col: cell.coord.col(),
1731                    new_value: LiteralValue::Empty,
1732                });
1733            }
1734        }
1735
1736        // Writes for new values (row-major to match target rectangle)
1737        if !target_cells.is_empty() {
1738            let first = target_cells.first().copied().unwrap();
1739            let row0 = first.coord.row();
1740            let col0 = first.coord.col();
1741            let sheet = self.sheet_name(first.sheet_id).to_string();
1742            for (r_off, row_vals) in values.iter().enumerate() {
1743                for (c_off, v) in row_vals.iter().enumerate() {
1744                    ops.push(Op {
1745                        sheet: sheet.clone(),
1746                        row: row0 + r_off as u32,
1747                        col: col0 + c_off as u32,
1748                        new_value: v.clone(),
1749                    });
1750                }
1751            }
1752        }
1753
1754        // Shadow buffer of old values for rollback
1755        #[derive(Clone)]
1756        struct OldVal {
1757            present: bool,
1758            value: LiteralValue,
1759        }
1760        let mut old_values: Vec<((String, u32, u32), OldVal)> = Vec::with_capacity(ops.len());
1761
1762        // Capture old values before applying
1763        for op in &ops {
1764            // op.row/op.col are internal 0-based; get_cell_value is a public 1-based API.
1765            let old = self
1766                .get_cell_value(&op.sheet, op.row + 1, op.col + 1)
1767                .unwrap_or(LiteralValue::Empty);
1768            let present = true; // unified model: we always treat as present
1769            old_values.push((
1770                (op.sheet.clone(), op.row, op.col),
1771                OldVal {
1772                    present,
1773                    value: old,
1774                },
1775            ));
1776        }
1777
1778        // Apply with optional injected fault
1779        for (applied, op) in ops.iter().enumerate() {
1780            if let Some(n) = fault_after_ops
1781                && applied == n
1782            {
1783                for idx in (0..applied).rev() {
1784                    let ((ref sheet, row, col), ref old) = old_values[idx];
1785                    let _ = self.set_cell_value(sheet, row + 1, col + 1, old.value.clone());
1786                }
1787                return Err(ExcelError::new(ExcelErrorKind::Error)
1788                    .with_message("Injected persistence fault during spill commit"));
1789            }
1790            let _ = self.set_cell_value(&op.sheet, op.row + 1, op.col + 1, op.new_value.clone());
1791        }
1792
1793        // Update spill ownership maps only on success
1794        // Clear previous ownership not reused
1795        for cell in prev_cells.iter() {
1796            if !new_set.contains(cell) {
1797                self.spill_cell_to_anchor.remove(cell);
1798            }
1799        }
1800        // Mark ownership for new rectangle using the declared target cells only
1801        for cell in &target_cells {
1802            self.spill_cell_to_anchor.insert(*cell, anchor);
1803        }
1804        self.spill_anchor_to_cells.insert(anchor, target_cells);
1805        Ok(())
1806    }
1807
1808    pub(crate) fn spill_cells_for_anchor(&self, anchor: VertexId) -> Option<&[CellRef]> {
1809        self.spill_anchor_to_cells
1810            .get(&anchor)
1811            .map(|v| v.as_slice())
1812    }
1813
1814    /// Clear an existing spill region for an anchor (set cells to Empty and forget ownership)
1815    pub fn clear_spill_region(&mut self, anchor: VertexId) {
1816        if let Some(cells) = self.spill_anchor_to_cells.remove(&anchor) {
1817            for cell in cells {
1818                let sheet = self.sheet_name(cell.sheet_id).to_string();
1819                let _ = self.set_cell_value(
1820                    &sheet,
1821                    cell.coord.row() + 1,
1822                    cell.coord.col() + 1,
1823                    LiteralValue::Empty,
1824                );
1825                self.spill_cell_to_anchor.remove(&cell);
1826            }
1827        }
1828    }
1829
1830    /// Check if a vertex exists
1831    pub(crate) fn vertex_exists(&self, vertex_id: VertexId) -> bool {
1832        if vertex_id.0 < FIRST_NORMAL_VERTEX {
1833            return false;
1834        }
1835        let index = (vertex_id.0 - FIRST_NORMAL_VERTEX) as usize;
1836        index < self.store.len()
1837    }
1838
1839    /// Get the kind of a vertex
1840    pub(crate) fn get_vertex_kind(&self, vertex_id: VertexId) -> VertexKind {
1841        self.store.kind(vertex_id)
1842    }
1843
1844    /// Get the sheet ID of a vertex
1845    pub(crate) fn get_vertex_sheet_id(&self, vertex_id: VertexId) -> SheetId {
1846        self.store.sheet_id(vertex_id)
1847    }
1848
1849    pub fn get_formula_id(&self, vertex_id: VertexId) -> Option<AstNodeId> {
1850        self.vertex_formulas.get(&vertex_id).copied()
1851    }
1852
1853    pub fn get_formula_id_and_volatile(&self, vertex_id: VertexId) -> Option<(AstNodeId, bool)> {
1854        let ast_id = self.get_formula_id(vertex_id)?;
1855        Some((ast_id, self.is_volatile(vertex_id)))
1856    }
1857
1858    pub fn get_formula_node(&self, vertex_id: VertexId) -> Option<&super::arena::AstNodeData> {
1859        let ast_id = self.get_formula_id(vertex_id)?;
1860        self.data_store.get_node(ast_id)
1861    }
1862
1863    pub fn get_formula_node_and_volatile(
1864        &self,
1865        vertex_id: VertexId,
1866    ) -> Option<(&super::arena::AstNodeData, bool)> {
1867        let (ast_id, vol) = self.get_formula_id_and_volatile(vertex_id)?;
1868        let node = self.data_store.get_node(ast_id)?;
1869        Some((node, vol))
1870    }
1871
1872    /// Get the formula AST for a vertex.
1873    ///
1874    /// Not used in hot paths; reconstructs from arena.
1875    pub fn get_formula(&self, vertex_id: VertexId) -> Option<ASTNode> {
1876        let ast_id = self.get_formula_id(vertex_id)?;
1877        self.data_store.retrieve_ast(ast_id, &self.sheet_reg)
1878    }
1879
1880    /// Get the value stored for a vertex
1881    pub fn get_value(&self, vertex_id: VertexId) -> Option<LiteralValue> {
1882        self.vertex_values
1883            .get(&vertex_id)
1884            .map(|&value_ref| self.data_store.retrieve_value(value_ref))
1885    }
1886
1887    /// Get the cell reference for a vertex
1888    pub(crate) fn get_cell_ref(&self, vertex_id: VertexId) -> Option<CellRef> {
1889        let packed_coord = self.store.coord(vertex_id);
1890        let sheet_id = self.store.sheet_id(vertex_id);
1891        let coord = Coord::new(packed_coord.row(), packed_coord.col(), true, true);
1892        Some(CellRef::new(sheet_id, coord))
1893    }
1894
1895    /// Create a cell reference (helper for internal use)
1896    pub(crate) fn make_cell_ref_internal(&self, sheet_id: SheetId, row: u32, col: u32) -> CellRef {
1897        let coord = Coord::new(row, col, true, true);
1898        CellRef::new(sheet_id, coord)
1899    }
1900
1901    /// Create a cell reference from sheet name and Excel 1-based coordinates.
1902    pub fn make_cell_ref(&self, sheet_name: &str, row: u32, col: u32) -> CellRef {
1903        let sheet_id = self.sheet_reg.get_id(sheet_name).unwrap_or(0);
1904        let coord = Coord::from_excel(row, col, true, true);
1905        CellRef::new(sheet_id, coord)
1906    }
1907
1908    /// Check if a vertex is dirty
1909    pub(crate) fn is_dirty(&self, vertex_id: VertexId) -> bool {
1910        self.store.is_dirty(vertex_id)
1911    }
1912
1913    /// Check if a vertex is volatile
1914    pub(crate) fn is_volatile(&self, vertex_id: VertexId) -> bool {
1915        self.store.is_volatile(vertex_id)
1916    }
1917
1918    /// Get vertex ID for a cell address
1919    pub fn get_vertex_id_for_address(&self, addr: &CellRef) -> Option<&VertexId> {
1920        self.cell_to_vertex.get(addr)
1921    }
1922
1923    #[cfg(test)]
1924    pub fn cell_to_vertex(&self) -> &FxHashMap<CellRef, VertexId> {
1925        &self.cell_to_vertex
1926    }
1927
1928    /// Get the dependencies of a vertex (for scheduler)
1929    pub(crate) fn get_dependencies(&self, vertex_id: VertexId) -> Vec<VertexId> {
1930        self.edges.out_edges(vertex_id)
1931    }
1932
1933    /// Check if a vertex has a self-loop
1934    pub(crate) fn has_self_loop(&self, vertex_id: VertexId) -> bool {
1935        self.edges.out_edges(vertex_id).contains(&vertex_id)
1936    }
1937
1938    /// Get dependents of a vertex (vertices that depend on this vertex)
1939    /// Uses reverse edges for O(1) lookup when available
1940    pub(crate) fn get_dependents(&self, vertex_id: VertexId) -> Vec<VertexId> {
1941        // If there are pending changes in delta, we need to scan
1942        // Otherwise we can use the fast reverse edges
1943        if self.edges.delta_size() > 0 {
1944            // Fall back to scanning when delta has changes
1945            let mut dependents = Vec::new();
1946            for (&_addr, &vid) in &self.cell_to_vertex {
1947                let out_edges = self.edges.out_edges(vid);
1948                if out_edges.contains(&vertex_id) {
1949                    dependents.push(vid);
1950                }
1951            }
1952            for named in self.named_ranges.values() {
1953                let vid = named.vertex;
1954                let out_edges = self.edges.out_edges(vid);
1955                if out_edges.contains(&vertex_id) {
1956                    dependents.push(vid);
1957                }
1958            }
1959            for named in self.sheet_named_ranges.values() {
1960                let vid = named.vertex;
1961                let out_edges = self.edges.out_edges(vid);
1962                if out_edges.contains(&vertex_id) {
1963                    dependents.push(vid);
1964                }
1965            }
1966            dependents
1967        } else {
1968            // Fast path: use reverse edges from CSR
1969            self.edges.in_edges(vertex_id).to_vec()
1970        }
1971    }
1972
1973    // Internal helper methods for Milestone 0.4
1974
1975    /// Internal: Create a snapshot of vertex state for rollback
1976    #[doc(hidden)]
1977    pub fn snapshot_vertex(&self, id: VertexId) -> crate::engine::VertexSnapshot {
1978        let coord = self.store.coord(id);
1979        let sheet_id = self.store.sheet_id(id);
1980        let kind = self.store.kind(id);
1981        let flags = self.store.flags(id);
1982
1983        // Get value and formula references
1984        let value_ref = self.vertex_values.get(&id).copied();
1985        let formula_ref = self.vertex_formulas.get(&id).copied();
1986
1987        // Get outgoing edges (dependencies)
1988        let out_edges = self.get_dependencies(id);
1989
1990        crate::engine::VertexSnapshot {
1991            coord,
1992            sheet_id,
1993            kind,
1994            flags,
1995            value_ref,
1996            formula_ref,
1997            out_edges,
1998        }
1999    }
2000
2001    /// Internal: Remove all edges for a vertex
2002    #[doc(hidden)]
2003    pub fn remove_all_edges(&mut self, id: VertexId) {
2004        // Enter batch mode to avoid intermediate rebuilds
2005        self.edges.begin_batch();
2006
2007        // Remove outgoing edges (this vertex's dependencies)
2008        self.remove_dependent_edges(id);
2009
2010        // Force rebuild to get accurate dependents list
2011        // This is necessary because get_dependents uses CSR reverse edges
2012        self.edges.rebuild();
2013
2014        // Remove incoming edges (vertices that depend on this vertex)
2015        let dependents = self.get_dependents(id);
2016        if self.pk_order.is_some()
2017            && let Some(mut pk) = self.pk_order.take()
2018        {
2019            for dependent in &dependents {
2020                pk.remove_edge(id, *dependent);
2021            }
2022            self.pk_order = Some(pk);
2023        }
2024        for dependent in dependents {
2025            self.edges.remove_edge(dependent, id);
2026        }
2027
2028        // Exit batch mode and rebuild once with all changes
2029        self.edges.end_batch();
2030    }
2031
2032    /// Internal: Mark vertex as having #REF! error
2033    #[doc(hidden)]
2034    pub fn mark_as_ref_error(&mut self, id: VertexId) {
2035        let error = LiteralValue::Error(ExcelError::new(ExcelErrorKind::Ref));
2036        let value_ref = self.data_store.store_value(error);
2037        self.vertex_values.insert(id, value_ref);
2038        self.store.set_dirty(id, true);
2039        self.dirty_vertices.insert(id);
2040    }
2041
2042    /// Check if a vertex has a #REF! error
2043    pub fn is_ref_error(&self, id: VertexId) -> bool {
2044        if let Some(value_ref) = self.vertex_values.get(&id) {
2045            let value = self.data_store.retrieve_value(*value_ref);
2046            if let LiteralValue::Error(err) = value {
2047                return err.kind == ExcelErrorKind::Ref;
2048            }
2049        }
2050        false
2051    }
2052
2053    /// Internal: Mark all direct dependents as dirty
2054    #[doc(hidden)]
2055    pub fn mark_dependents_dirty(&mut self, id: VertexId) {
2056        let dependents = self.get_dependents(id);
2057        for dep_id in dependents {
2058            self.store.set_dirty(dep_id, true);
2059            self.dirty_vertices.insert(dep_id);
2060        }
2061    }
2062
2063    /// Internal: Mark a vertex as volatile
2064    #[doc(hidden)]
2065    pub fn mark_volatile(&mut self, id: VertexId, volatile: bool) {
2066        self.store.set_volatile(id, volatile);
2067        if volatile {
2068            self.volatile_vertices.insert(id);
2069        } else {
2070            self.volatile_vertices.remove(&id);
2071        }
2072    }
2073
2074    /// Update vertex coordinate
2075    #[doc(hidden)]
2076    pub fn set_coord(&mut self, id: VertexId, coord: AbsCoord) {
2077        self.store.set_coord(id, coord);
2078    }
2079
2080    /// Update edge cache coordinate
2081    #[doc(hidden)]
2082    pub fn update_edge_coord(&mut self, id: VertexId, coord: AbsCoord) {
2083        self.edges.update_coord(id, coord);
2084    }
2085
2086    /// Mark vertex as deleted (tombstone)
2087    #[doc(hidden)]
2088    pub fn mark_deleted(&mut self, id: VertexId, deleted: bool) {
2089        self.store.mark_deleted(id, deleted);
2090    }
2091
2092    /// Set vertex kind
2093    #[doc(hidden)]
2094    pub fn set_kind(&mut self, id: VertexId, kind: VertexKind) {
2095        self.store.set_kind(id, kind);
2096    }
2097
2098    /// Set vertex dirty flag
2099    #[doc(hidden)]
2100    pub fn set_dirty(&mut self, id: VertexId, dirty: bool) {
2101        self.store.set_dirty(id, dirty);
2102        if dirty {
2103            self.dirty_vertices.insert(id);
2104        } else {
2105            self.dirty_vertices.remove(&id);
2106        }
2107    }
2108
2109    /// Get vertex kind (for testing)
2110    #[cfg(test)]
2111    pub(crate) fn get_kind(&self, id: VertexId) -> VertexKind {
2112        self.store.kind(id)
2113    }
2114
2115    /// Get vertex flags (for testing)
2116    #[cfg(test)]
2117    pub(crate) fn get_flags(&self, id: VertexId) -> u8 {
2118        self.store.flags(id)
2119    }
2120
2121    /// Check if vertex is deleted (for testing)
2122    #[cfg(test)]
2123    pub(crate) fn is_deleted(&self, id: VertexId) -> bool {
2124        self.store.is_deleted(id)
2125    }
2126
2127    /// Force edge rebuild (internal use)
2128    #[doc(hidden)]
2129    pub fn rebuild_edges(&mut self) {
2130        self.edges.rebuild();
2131    }
2132
2133    /// Get delta size (internal use)
2134    #[doc(hidden)]
2135    pub fn edges_delta_size(&self) -> usize {
2136        self.edges.delta_size()
2137    }
2138
2139    /// Get vertex ID for specific cell address
2140    pub fn get_vertex_for_cell(&self, addr: &CellRef) -> Option<VertexId> {
2141        self.cell_to_vertex.get(addr).copied()
2142    }
2143
2144    /// Get coord for a vertex (public for VertexEditor)
2145    pub fn get_coord(&self, id: VertexId) -> AbsCoord {
2146        self.store.coord(id)
2147    }
2148
2149    /// Get sheet_id for a vertex (public for VertexEditor)
2150    pub fn get_sheet_id(&self, id: VertexId) -> SheetId {
2151        self.store.sheet_id(id)
2152    }
2153
2154    /// Get all vertices in a sheet
2155    pub fn vertices_in_sheet(&self, sheet_id: SheetId) -> impl Iterator<Item = VertexId> + '_ {
2156        self.store
2157            .all_vertices()
2158            .filter(move |&id| self.vertex_exists(id) && self.store.sheet_id(id) == sheet_id)
2159    }
2160
2161    /// Does a vertex have a formula associated
2162    pub fn vertex_has_formula(&self, id: VertexId) -> bool {
2163        self.vertex_formulas.contains_key(&id)
2164    }
2165
2166    /// Get all vertices with formulas
2167    pub fn vertices_with_formulas(&self) -> impl Iterator<Item = VertexId> + '_ {
2168        self.vertex_formulas.keys().copied()
2169    }
2170
2171    /// Update a vertex's formula
2172    pub fn update_vertex_formula(&mut self, id: VertexId, ast: ASTNode) -> Result<(), ExcelError> {
2173        // Get the sheet_id for this vertex
2174        let sheet_id = self.store.sheet_id(id);
2175
2176        // If the adjusted AST contains special #REF markers (from structural edits),
2177        // treat this as a REF error on the vertex instead of attempting to resolve.
2178        // This prevents failures when reference_adjuster injected placeholder refs.
2179        let has_ref_marker = ast.get_dependencies().into_iter().any(|r| {
2180            matches!(
2181                r,
2182                ReferenceType::Cell { sheet: Some(s), .. }
2183                    | ReferenceType::Range { sheet: Some(s), .. } if s == "#REF"
2184            )
2185        });
2186        if has_ref_marker {
2187            // Store the adjusted AST for round-tripping/display, but set value state to #REF!
2188            let ast_id = self.data_store.store_ast(&ast, &self.sheet_reg);
2189            self.vertex_formulas.insert(id, ast_id);
2190            self.mark_as_ref_error(id);
2191            self.store.set_kind(id, VertexKind::FormulaScalar);
2192            return Ok(());
2193        }
2194
2195        // Extract dependencies from AST
2196        let (new_dependencies, new_range_dependencies, _, named_dependencies) =
2197            self.extract_dependencies(&ast, sheet_id)?;
2198
2199        // Remove old dependencies first
2200        self.remove_dependent_edges(id);
2201        self.detach_vertex_from_names(id);
2202
2203        // Store the new formula
2204        let ast_id = self.data_store.store_ast(&ast, &self.sheet_reg);
2205        self.vertex_formulas.insert(id, ast_id);
2206
2207        // Add new dependency edges
2208        self.add_dependent_edges(id, &new_dependencies);
2209        self.add_range_dependent_edges(id, &new_range_dependencies, sheet_id);
2210
2211        if !named_dependencies.is_empty() {
2212            self.attach_vertex_to_names(id, &named_dependencies);
2213        }
2214
2215        // Mark as formula vertex
2216        self.store.set_kind(id, VertexKind::FormulaScalar);
2217
2218        Ok(())
2219    }
2220
2221    /// Mark a vertex as dirty without propagation (for VertexEditor)
2222    pub fn mark_vertex_dirty(&mut self, vertex_id: VertexId) {
2223        self.store.set_dirty(vertex_id, true);
2224        self.dirty_vertices.insert(vertex_id);
2225    }
2226
2227    /// Update cell mapping for a vertex (for VertexEditor)
2228    pub fn update_cell_mapping(
2229        &mut self,
2230        id: VertexId,
2231        old_addr: Option<CellRef>,
2232        new_addr: CellRef,
2233    ) {
2234        // Remove old mapping if it exists
2235        if let Some(old) = old_addr {
2236            self.cell_to_vertex.remove(&old);
2237        }
2238        // Add new mapping
2239        self.cell_to_vertex.insert(new_addr, id);
2240    }
2241
2242    /// Remove cell mapping (for VertexEditor)
2243    pub fn remove_cell_mapping(&mut self, addr: &CellRef) {
2244        self.cell_to_vertex.remove(addr);
2245    }
2246
2247    /// Get the cell reference for a vertex
2248    pub fn get_cell_ref_for_vertex(&self, id: VertexId) -> Option<CellRef> {
2249        let coord = self.store.coord(id);
2250        let sheet_id = self.store.sheet_id(id);
2251        // Find the cell reference in the mapping
2252        let cell_ref = CellRef::new(sheet_id, Coord::new(coord.row(), coord.col(), true, true));
2253        // Verify it actually maps to this vertex
2254        if self.cell_to_vertex.get(&cell_ref) == Some(&id) {
2255            Some(cell_ref)
2256        } else {
2257            None
2258        }
2259    }
2260
2261    // ========== Phase 2: Structural Operations ==========
2262}
2263
2264// ========== Sheet Management Operations ==========