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