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