Skip to main content

formualizer_eval/engine/graph/
mod.rs

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