Skip to main content

formualizer_eval/engine/
eval.rs

1use crate::SheetId;
2use crate::arrow_store::SheetStore;
3use crate::engine::eval_delta::{DeltaCollector, DeltaMode, EvalDelta};
4use crate::engine::named_range::{NameScope, NamedDefinition};
5use crate::engine::range_view::RangeView;
6use crate::engine::row_visibility::RowVisibilityState;
7use crate::engine::spill::{RegionLockManager, SpillMeta, SpillShape};
8use crate::engine::virtual_deps::VirtualDepBuilder;
9use crate::engine::{
10    DependencyGraph, EvalConfig, FormulaParseDiagnostic, FormulaParsePolicy, RowVisibilitySource,
11    Scheduler, VertexId, VertexKind, VisibilityMaskMode,
12};
13use crate::interpreter::Interpreter;
14use crate::reference::{CellRef, Coord, RangeRef};
15use crate::traits::FunctionProvider;
16use crate::traits::{EvaluationContext, Resolver};
17use chrono::Timelike;
18use formualizer_common::{col_letters_from_1based, parse_a1_1based};
19use formualizer_parse::parser::ReferenceType;
20use formualizer_parse::{ASTNode, ASTNodeType, ExcelError, ExcelErrorKind, LiteralValue};
21use rayon::ThreadPoolBuilder;
22use rustc_hash::{FxHashMap, FxHashSet};
23use std::sync::Arc;
24use std::sync::atomic::{AtomicBool, Ordering};
25
26type StagedFormulaEntry = (u32, u32, String);
27type ParsedFormulaEntry = (u32, u32, ASTNode);
28type StagedFormulaMap = std::collections::HashMap<String, Vec<StagedFormulaEntry>>;
29type PreparedFormulaBatches = Vec<(String, Vec<ParsedFormulaEntry>)>;
30type StagedFormulaBatches = Vec<(String, Vec<StagedFormulaEntry>)>;
31
32pub struct Engine<R> {
33    pub(crate) graph: DependencyGraph,
34    resolver: R,
35    pub config: EvalConfig,
36    workbook_load_limits: crate::engine::WorkbookLoadLimits,
37    clock: Arc<dyn crate::timezone::ClockProvider>,
38    thread_pool: Option<Arc<rayon::ThreadPool>>,
39    pub recalc_epoch: u64,
40    snapshot_id: std::sync::atomic::AtomicU64,
41    topology_epoch: u64,
42    cached_static_schedule: Option<CachedScheduleEntry>,
43    spill_mgr: ShimSpillManager,
44    /// Arrow-backed storage for sheet values (Phase A)
45    arrow_sheets: SheetStore,
46    /// True if any edit after bulk load; disables Arrow reads for parity
47    has_edited: bool,
48    /// Overlay compaction counter (Phase C instrumentation)
49    overlay_compactions: u64,
50
51    // Overlay memory observability / budget (ticket 503)
52    computed_overlay_bytes_estimate: usize,
53    computed_overlay_mirroring_disabled: bool,
54    /// When true, RangeView resolution materializes from graph/Arrow base per-cell.
55    /// This preserves correctness if we stop mirroring formula/spill outputs into computed overlays.
56    pub(crate) force_materialize_range_views: bool,
57    // Pass-scoped cache for Arrow used-row bounds per column
58    row_bounds_cache: std::sync::RwLock<Option<RowBoundsCache>>,
59    source_cache: Arc<std::sync::RwLock<SourceCache>>,
60    /// Staged formulas by sheet when `defer_graph_building` is enabled.
61    staged_formulas: StagedFormulaMap,
62    /// Per-sheet row visibility sidecar state.
63    row_visibility: FxHashMap<SheetId, RowVisibilityState>,
64    /// Cached row visibility masks keyed by sheet/span/mode/version.
65    row_visibility_mask_cache: std::sync::RwLock<
66        FxHashMap<VisibilityMaskCacheKey, std::sync::Arc<arrow_array::BooleanArray>>,
67    >,
68    /// Non-fatal malformed formula diagnostics captured during ingest/graph-build.
69    formula_parse_diagnostics: Vec<FormulaParseDiagnostic>,
70    /// Transient cancellation flag used during evaluation
71    active_cancel_flag: Option<Arc<AtomicBool>>,
72
73    /// Engine-level action depth.
74    ///
75    /// Ticket 614 introduces `Engine::action` as a stable, commit-only transaction surface.
76    /// Nested actions are currently disallowed (deterministic rule) and will return an error.
77    action_depth: u32,
78
79    // Phase 3b virtual-dependency convergence telemetry
80    last_virtual_dep_telemetry: VirtualDepTelemetry,
81    virtual_dep_fallback_activations: u64,
82}
83
84/// Minimal edit surface used by `Engine::action`.
85///
86/// This wrapper is intentionally thin for ticket 614 (commit-only): it delegates to existing
87/// `Engine` edit methods and does not create changelog boundaries or implement rollback.
88pub struct EngineAction<'a, R>
89where
90    R: EvaluationContext,
91{
92    engine: &'a mut Engine<R>,
93    name: String,
94    // Optional external ChangeLog pointer used by `Engine::action_with_logger`.
95    // Stored as a raw pointer to avoid creating aliasing `&mut` borrows alongside `&mut Engine`.
96    log: Option<*mut crate::engine::ChangeLog>,
97    // Optional Arrow undo journal used by `Engine::action_atomic`.
98    // Stored as a raw pointer to avoid aliasing issues with `&mut Engine`.
99    arrow_undo: Option<*mut crate::engine::ArrowUndoBatch>,
100    // True when this EngineAction must enforce conservative atomic transaction policy.
101    atomic_policy: bool,
102}
103
104impl<'a, R> EngineAction<'a, R>
105where
106    R: EvaluationContext,
107{
108    #[inline]
109    fn addr_for(&mut self, sheet: &str, row: u32, col: u32) -> crate::reference::CellRef {
110        let sheet_id = self.engine.graph.sheet_id_mut(sheet);
111        let coord = crate::reference::Coord::from_excel(row, col, true, true);
112        crate::reference::CellRef::new(sheet_id, coord)
113    }
114
115    #[inline]
116    pub fn name(&self) -> &str {
117        &self.name
118    }
119
120    #[inline]
121    pub fn set_cell_value(
122        &mut self,
123        sheet: &str,
124        row: u32,
125        col: u32,
126        value: LiteralValue,
127    ) -> Result<(), crate::engine::EditorError> {
128        if self.log.is_some() {
129            let old_value = self.engine.read_cell_value(sheet, row, col);
130            let old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
131            let addr = self.addr_for(sheet, row, col);
132            let Some(log_ptr) = self.log else {
133                return Err(crate::engine::EditorError::TransactionFailed {
134                    reason: "action_with_logger: missing ChangeLog".to_string(),
135                });
136            };
137
138            // For atomic journal mode, record computed overlay effects for this cell.
139            // Delta-overlay undo is recorded semantically based on old_value/old_formula.
140            let old_comp = if self.arrow_undo.is_some() {
141                self.engine.read_computed_overlay_cell(sheet, row, col)
142            } else {
143                None
144            };
145
146            let delta_old_sem = if old_formula.is_some() {
147                None
148            } else {
149                Some(old_value.clone().unwrap_or(LiteralValue::Empty))
150            };
151
152            let start_len = unsafe { (&*log_ptr).len() };
153
154            // Safety: `log_ptr` comes from a unique `&mut ChangeLog` in `Engine::action_with_logger`.
155            let log = unsafe { &mut *log_ptr };
156            self.engine.edit_with_logger(log, |editor| {
157                editor.set_cell_value(addr, value.clone());
158            });
159            log.patch_last_cell_event_old_state(addr, old_value.clone(), old_formula.clone());
160
161            if let Some(undo_ptr) = self.arrow_undo {
162                // 1) Spill snapshot operations (computed overlay rect restore).
163                let new_events = &unsafe { (&*log_ptr).events() }[start_len..];
164                let undo = unsafe { &mut *undo_ptr };
165                self.engine
166                    .record_spill_ops_into_arrow_undo(undo, new_events);
167
168                // 2) Delta/computed overlay single-cell deltas.
169                let new_comp = self.engine.read_computed_overlay_cell(sheet, row, col);
170                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
171                let row0 = row.saturating_sub(1);
172                let col0 = col.saturating_sub(1);
173                let delta_new_sem = Some(value.clone());
174                undo.record_delta_cell(sheet_id, row0, col0, delta_old_sem, delta_new_sem);
175                undo.record_computed_cell(sheet_id, row0, col0, old_comp, new_comp);
176            }
177            Ok(())
178        } else {
179            self.engine
180                .set_cell_value(sheet, row, col, value)
181                .map_err(crate::engine::EditorError::from)
182        }
183    }
184
185    #[inline]
186    pub fn set_cell_formula(
187        &mut self,
188        sheet: &str,
189        row: u32,
190        col: u32,
191        ast: ASTNode,
192    ) -> Result<(), crate::engine::EditorError> {
193        if self.log.is_some() {
194            let old_value = self.engine.read_cell_value(sheet, row, col);
195            let old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
196            let addr = self.addr_for(sheet, row, col);
197            let Some(log_ptr) = self.log else {
198                return Err(crate::engine::EditorError::TransactionFailed {
199                    reason: "action_with_logger: missing ChangeLog".to_string(),
200                });
201            };
202
203            let delta_old = if self.arrow_undo.is_some() {
204                if old_formula.is_some() {
205                    None
206                } else {
207                    Some(old_value.clone().unwrap_or(LiteralValue::Empty))
208                }
209            } else {
210                None
211            };
212            let start_len = unsafe { (&*log_ptr).len() };
213
214            // Safety: `log_ptr` comes from a unique `&mut ChangeLog` in `Engine::action_with_logger`.
215            let log = unsafe { &mut *log_ptr };
216            self.engine.edit_with_logger(log, |editor| {
217                editor.set_cell_formula(addr, ast.clone());
218            });
219            log.patch_last_cell_event_old_state(addr, old_value, old_formula);
220
221            if let Some(undo_ptr) = self.arrow_undo {
222                let new_events = &unsafe { (&*log_ptr).events() }[start_len..];
223                let undo = unsafe { &mut *undo_ptr };
224                self.engine
225                    .record_spill_ops_into_arrow_undo(undo, new_events);
226                let delta_new: Option<LiteralValue> = None;
227                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
228                let row0 = row.saturating_sub(1);
229                let col0 = col.saturating_sub(1);
230                undo.record_delta_cell(sheet_id, row0, col0, delta_old, delta_new);
231            }
232            Ok(())
233        } else {
234            self.engine
235                .set_cell_formula(sheet, row, col, ast)
236                .map_err(crate::engine::EditorError::from)
237        }
238    }
239
240    #[inline]
241    pub fn set_row_hidden(
242        &mut self,
243        sheet: &str,
244        row_1based: u32,
245        hidden: bool,
246        source: RowVisibilitySource,
247    ) -> Result<(), crate::engine::EditorError> {
248        if self.log.is_some() {
249            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
250            let row0 = Engine::<R>::normalize_row_1based(row_1based)?;
251            let old_hidden = self
252                .engine
253                .row_visibility
254                .get(&sheet_id)
255                .map(|state| state.is_row_hidden(row0, Some(source)))
256                .unwrap_or(false);
257            if old_hidden == hidden {
258                return Ok(());
259            }
260
261            let _ = self
262                .engine
263                .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
264
265            let Some(log_ptr) = self.log else {
266                return Err(crate::engine::EditorError::TransactionFailed {
267                    reason: "action_with_logger: missing ChangeLog".to_string(),
268                });
269            };
270            unsafe { &mut *log_ptr }.record(crate::engine::ChangeEvent::SetRowVisibility {
271                sheet_id,
272                row0,
273                source,
274                old_hidden,
275                new_hidden: hidden,
276            });
277
278            Ok(())
279        } else {
280            self.engine
281                .set_row_hidden(sheet, row_1based, hidden, source)
282        }
283    }
284
285    #[inline]
286    pub fn set_rows_hidden(
287        &mut self,
288        sheet: &str,
289        start_row_1based: u32,
290        end_row_1based: u32,
291        hidden: bool,
292        source: RowVisibilitySource,
293    ) -> Result<(), crate::engine::EditorError> {
294        if self.log.is_some() {
295            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
296            let (start_row0, end_row0) =
297                Engine::<R>::normalize_row_range_1based(start_row_1based, end_row_1based)?;
298
299            let Some(log_ptr) = self.log else {
300                return Err(crate::engine::EditorError::TransactionFailed {
301                    reason: "action_with_logger: missing ChangeLog".to_string(),
302                });
303            };
304            let log = unsafe { &mut *log_ptr };
305
306            for row0 in start_row0..=end_row0 {
307                let old_hidden = self
308                    .engine
309                    .row_visibility
310                    .get(&sheet_id)
311                    .map(|state| state.is_row_hidden(row0, Some(source)))
312                    .unwrap_or(false);
313                if old_hidden == hidden {
314                    continue;
315                }
316
317                let _ = self
318                    .engine
319                    .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
320
321                log.record(crate::engine::ChangeEvent::SetRowVisibility {
322                    sheet_id,
323                    row0,
324                    source,
325                    old_hidden,
326                    new_hidden: hidden,
327                });
328            }
329
330            Ok(())
331        } else {
332            self.engine
333                .set_rows_hidden(sheet, start_row_1based, end_row_1based, hidden, source)
334        }
335    }
336
337    #[inline]
338    pub fn insert_rows(
339        &mut self,
340        sheet: &str,
341        before: u32,
342        count: u32,
343    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
344        if self.log.is_some() {
345            let Some(log_ptr) = self.log else {
346                return Err(crate::engine::EditorError::TransactionFailed {
347                    reason: "action_atomic: missing ChangeLog".to_string(),
348                });
349            };
350
351            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
352            let before0 = before.saturating_sub(1);
353
354            // Graph structural insert (logged) - no snapshot bump.
355            let summary = {
356                let log = unsafe { &mut *log_ptr };
357                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
358                    Ok(crate::engine::ShiftSummary::default());
359                self.engine.edit_with_logger(log, |editor| {
360                    out = editor.insert_rows(sheet_id, before0, count);
361                });
362                out?
363            };
364
365            // Arrow insert (truth) + undo op.
366            self.engine.ensure_arrow_sheet(sheet);
367            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
368                asheet.insert_rows(before0 as usize, count as usize);
369            }
370            self.engine
371                .shift_row_visibility_insert(sheet_id, before0, count);
372            if let Some(undo_ptr) = self.arrow_undo {
373                unsafe { &mut *undo_ptr }.record_insert_rows(sheet_id, before0, count);
374            }
375            Ok(summary)
376        } else {
377            self.engine.insert_rows(sheet, before, count)
378        }
379    }
380
381    #[inline]
382    pub fn delete_rows(
383        &mut self,
384        sheet: &str,
385        start: u32,
386        count: u32,
387    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
388        if self.atomic_policy {
389            return Err(crate::engine::EditorError::TransactionUnsupported {
390                reason:
391                    "delete_rows is not supported inside atomic actions (conservative rollback policy)"
392                        .to_string(),
393            });
394        }
395        self.engine.delete_rows(sheet, start, count)
396    }
397
398    #[inline]
399    pub fn insert_columns(
400        &mut self,
401        sheet: &str,
402        before: u32,
403        count: u32,
404    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
405        if self.log.is_some() {
406            let Some(log_ptr) = self.log else {
407                return Err(crate::engine::EditorError::TransactionFailed {
408                    reason: "action_atomic: missing ChangeLog".to_string(),
409                });
410            };
411
412            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
413            let before0 = before.saturating_sub(1);
414
415            let summary = {
416                let log = unsafe { &mut *log_ptr };
417                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
418                    Ok(crate::engine::ShiftSummary::default());
419                self.engine.edit_with_logger(log, |editor| {
420                    out = editor.insert_columns(sheet_id, before0, count);
421                });
422                out?
423            };
424
425            self.engine.ensure_arrow_sheet(sheet);
426            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
427                asheet.insert_columns(before0 as usize, count as usize);
428            }
429            if let Some(undo_ptr) = self.arrow_undo {
430                unsafe { &mut *undo_ptr }.record_insert_cols(sheet_id, before0, count);
431            }
432            Ok(summary)
433        } else {
434            self.engine.insert_columns(sheet, before, count)
435        }
436    }
437
438    #[inline]
439    pub fn delete_columns(
440        &mut self,
441        sheet: &str,
442        start: u32,
443        count: u32,
444    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
445        if self.atomic_policy {
446            return Err(crate::engine::EditorError::TransactionUnsupported {
447                reason:
448                    "delete_columns is not supported inside atomic actions (conservative rollback policy)"
449                        .to_string(),
450            });
451        }
452        self.engine.delete_columns(sheet, start, count)
453    }
454
455    /// Start an action from within an action.
456    ///
457    /// Nested actions are currently disallowed (ticket 614), so this will return a
458    /// `EditorError::TransactionFailed` while an outer action is active.
459    #[inline]
460    pub fn action<T>(
461        &mut self,
462        name: impl AsRef<str>,
463        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
464    ) -> Result<T, crate::engine::EditorError> {
465        self.engine.action(name, f)
466    }
467}
468
469struct ActionDepthGuard<'a, R> {
470    engine: *mut Engine<R>,
471    _marker: std::marker::PhantomData<&'a mut Engine<R>>,
472}
473
474impl<'a, R> Drop for ActionDepthGuard<'a, R> {
475    fn drop(&mut self) {
476        // Safety: the guard is created from a unique `&mut Engine` borrow and lives no longer
477        // than the surrounding `Engine::action` call.
478        unsafe {
479            let e = &mut *self.engine;
480            e.action_depth = e.action_depth.saturating_sub(1);
481        }
482    }
483}
484
485#[derive(Default)]
486struct SourceCache {
487    scalars: FxHashMap<(String, Option<u64>), LiteralValue>,
488    tables: FxHashMap<(String, Option<u64>), Arc<dyn crate::traits::Table>>,
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
492struct VisibilityMaskCacheKey {
493    sheet_id: SheetId,
494    start_row0: u32,
495    end_row0: u32,
496    mode: VisibilityMaskMode,
497    version: u64,
498}
499
500struct SourceCacheSession {
501    cache: Arc<std::sync::RwLock<SourceCache>>,
502}
503
504impl Drop for SourceCacheSession {
505    fn drop(&mut self) {
506        if let Ok(mut g) = self.cache.write() {
507            *g = SourceCache::default();
508        }
509    }
510}
511
512#[derive(Debug)]
513pub struct EvalResult {
514    pub computed_vertices: usize,
515    pub cycle_errors: usize,
516    pub elapsed: std::time::Duration,
517}
518
519#[derive(Debug, Clone, Default)]
520pub struct VirtualDepTelemetry {
521    pub candidate_vertices_total: usize,
522    pub vdeps_vertices_total: usize,
523    pub vdeps_edges_total: usize,
524    pub builder_elapsed_ms_total: u128,
525    pub schedule_virtual_passes: usize,
526    pub schedule_static_passes: usize,
527    pub schedule_cache_hits: usize,
528    pub schedule_cache_misses: usize,
529    pub reused_schedule_vertices_total: usize,
530    pub replan_iterations: usize,
531    pub changed_vdeps_total: usize,
532    pub bailout_reason: Option<&'static str>,
533    pub fallback_mode_activations: u64,
534}
535
536#[derive(Debug, Clone, Copy)]
537struct ScheduleBuildMeta {
538    candidate_vertices: usize,
539    vdeps_vertices: usize,
540    vdeps_edges: usize,
541    builder_elapsed_ms: u128,
542    used_virtual_schedule: bool,
543    schedule_cache_hit: bool,
544    schedule_cache_eligible: bool,
545}
546
547#[derive(Debug, Clone)]
548struct CachedScheduleEntry {
549    topology_epoch: u64,
550    candidate_vertices: Vec<VertexId>,
551    schedule: crate::engine::scheduler::Schedule,
552}
553
554type ScheduleBuildOutput = (
555    crate::engine::scheduler::Schedule,
556    FxHashMap<VertexId, Vec<VertexId>>,
557    ScheduleBuildMeta,
558);
559
560/// Cached evaluation schedule that can be replayed across multiple recalculations.
561#[derive(Debug)]
562pub struct RecalcPlan {
563    schedule: crate::engine::Schedule,
564    has_dynamic_refs: bool,
565}
566
567impl RecalcPlan {
568    pub fn layer_count(&self) -> usize {
569        self.schedule.layers.len()
570    }
571
572    pub fn has_dynamic_refs(&self) -> bool {
573        self.has_dynamic_refs
574    }
575}
576
577#[cfg(test)]
578pub(crate) mod criteria_mask_test_hooks {
579    use std::cell::Cell;
580
581    thread_local! {
582        static TEXT_SEGMENTS_TOTAL: Cell<usize> = const { Cell::new(0) };
583        static TEXT_SEGMENTS_ALL_NULL: Cell<usize> = const { Cell::new(0) };
584    }
585
586    pub fn reset_text_segment_counters() {
587        TEXT_SEGMENTS_TOTAL.with(|c| c.set(0));
588        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(0));
589    }
590
591    pub fn text_segment_counters() -> (usize, usize) {
592        let a = TEXT_SEGMENTS_TOTAL.with(|c| c.get());
593        let b = TEXT_SEGMENTS_ALL_NULL.with(|c| c.get());
594        (a, b)
595    }
596
597    pub(crate) fn inc_total() {
598        TEXT_SEGMENTS_TOTAL.with(|c| c.set(c.get() + 1));
599    }
600    pub(crate) fn inc_all_null() {
601        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(c.get() + 1));
602    }
603}
604
605#[cfg(test)]
606pub(crate) mod visibility_mask_test_hooks {
607    use std::cell::Cell;
608
609    thread_local! {
610        static HITS: Cell<usize> = const { Cell::new(0) };
611        static MISSES: Cell<usize> = const { Cell::new(0) };
612        static EVICTIONS: Cell<usize> = const { Cell::new(0) };
613    }
614
615    pub fn reset() {
616        HITS.with(|c| c.set(0));
617        MISSES.with(|c| c.set(0));
618        EVICTIONS.with(|c| c.set(0));
619    }
620
621    pub fn counters() -> (usize, usize, usize) {
622        let hits = HITS.with(|c| c.get());
623        let misses = MISSES.with(|c| c.get());
624        let evictions = EVICTIONS.with(|c| c.get());
625        (hits, misses, evictions)
626    }
627
628    pub(crate) fn inc_hit() {
629        HITS.with(|c| c.set(c.get() + 1));
630    }
631
632    pub(crate) fn inc_miss() {
633        MISSES.with(|c| c.set(c.get() + 1));
634    }
635
636    pub(crate) fn inc_eviction() {
637        EVICTIONS.with(|c| c.set(c.get() + 1));
638    }
639}
640
641fn compute_criteria_mask(
642    view: &RangeView<'_>,
643    col_in_view: usize,
644    pred: &crate::args::CriteriaPredicate,
645) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
646    use crate::compute_prelude::{boolean, cmp, concat_arrays};
647    use arrow::compute::kernels::comparison::{ilike, nilike};
648    use arrow_array::{
649        Array as _, ArrayRef, BooleanArray, Float64Array, StringArray, builder::BooleanBuilder,
650    };
651
652    // Helper: apply a numeric predicate to a single Float64Array chunk
653    fn apply_numeric_pred(
654        chunk: &Float64Array,
655        pred: &crate::args::CriteriaPredicate,
656    ) -> Option<BooleanArray> {
657        match pred {
658            crate::args::CriteriaPredicate::Gt(n) => {
659                cmp::gt(chunk, &Float64Array::new_scalar(*n)).ok()
660            }
661            crate::args::CriteriaPredicate::Ge(n) => {
662                cmp::gt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
663            }
664            crate::args::CriteriaPredicate::Lt(n) => {
665                cmp::lt(chunk, &Float64Array::new_scalar(*n)).ok()
666            }
667            crate::args::CriteriaPredicate::Le(n) => {
668                cmp::lt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
669            }
670            crate::args::CriteriaPredicate::Eq(v) => match v {
671                formualizer_common::LiteralValue::Number(x) => {
672                    cmp::eq(chunk, &Float64Array::new_scalar(*x)).ok()
673                }
674                formualizer_common::LiteralValue::Int(i) => {
675                    cmp::eq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
676                }
677                _ => None,
678            },
679            crate::args::CriteriaPredicate::Ne(v) => match v {
680                formualizer_common::LiteralValue::Number(x) => {
681                    cmp::neq(chunk, &Float64Array::new_scalar(*x)).ok()
682                }
683                formualizer_common::LiteralValue::Int(i) => {
684                    cmp::neq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
685                }
686                _ => None,
687            },
688            _ => None,
689        }
690    }
691
692    // Check if this is a numeric predicate that can be applied per-chunk
693    let is_numeric_pred = matches!(
694        pred,
695        crate::args::CriteriaPredicate::Gt(_)
696            | crate::args::CriteriaPredicate::Ge(_)
697            | crate::args::CriteriaPredicate::Lt(_)
698            | crate::args::CriteriaPredicate::Le(_)
699            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Number(_))
700            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Int(_))
701            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Number(_))
702            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Int(_))
703    );
704
705    // OPTIMIZED PATH: For numeric predicates, apply per-chunk and concatenate boolean masks.
706    // This avoids materializing the full numeric column (64-bit per element) and instead
707    // concatenates boolean masks (1-bit per element) - a 64x memory reduction.
708    if is_numeric_pred {
709        let mut bool_parts: Vec<BooleanArray> = Vec::new();
710        for res in view.numbers_slices() {
711            let (_rs, _rl, cols_seg) = res.ok()?;
712            if col_in_view < cols_seg.len() {
713                let chunk = cols_seg[col_in_view].as_ref();
714                let mask = apply_numeric_pred(chunk, pred)?;
715                bool_parts.push(mask);
716            }
717        }
718
719        if bool_parts.is_empty() {
720            return None;
721        } else if bool_parts.len() == 1 {
722            return Some(std::sync::Arc::new(bool_parts.remove(0)));
723        } else {
724            // Concatenate boolean masks (much cheaper than concatenating Float64 arrays)
725            let anys: Vec<&dyn arrow_array::Array> = bool_parts
726                .iter()
727                .map(|a| a as &dyn arrow_array::Array)
728                .collect();
729            let conc: ArrayRef = concat_arrays(&anys).ok()?;
730            let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
731            return Some(std::sync::Arc::new(ba));
732        }
733    }
734
735    // TEXT PATH: build masks per row-chunk using lowered text slices.
736    // This avoids concatenating full-string columns just to compute a boolean mask.
737    let (text_kind, text_pat, empty_special) = match pred {
738        crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(t)) => {
739            (0u8, t.to_lowercase(), t.is_empty())
740        }
741        crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Text(t)) => {
742            (1u8, t.to_lowercase(), false)
743        }
744        crate::args::CriteriaPredicate::TextLike {
745            pattern,
746            case_insensitive,
747        } => {
748            let p = if *case_insensitive {
749                pattern.to_lowercase()
750            } else {
751                pattern.clone()
752            };
753            (2u8, p.replace('*', "%").replace('?', "_"), false)
754        }
755        _ => return None,
756    };
757
758    let pat = StringArray::new_scalar(text_pat);
759    let mut bool_parts: Vec<BooleanArray> = Vec::new();
760
761    for res in view.iter_row_chunks() {
762        let cs = res.ok()?;
763        if cs.row_len == 0 {
764            continue;
765        }
766        #[cfg(test)]
767        criteria_mask_test_hooks::inc_total();
768
769        let slices = view.slice_lowered_text(cs.row_start, cs.row_len);
770        if col_in_view >= slices.len() {
771            return None;
772        }
773
774        let seg_opt = slices[col_in_view].as_ref().map(|a| a.as_ref());
775        let seg = match seg_opt {
776            Some(s) => s,
777            None => {
778                #[cfg(test)]
779                criteria_mask_test_hooks::inc_all_null();
780                if text_kind == 0 && empty_special {
781                    // Eq("") treats nulls (Empty) as equal.
782                    let mut bb = BooleanBuilder::with_capacity(cs.row_len);
783                    bb.append_n(cs.row_len, true);
784                    bool_parts.push(bb.finish());
785                } else {
786                    // For non-empty patterns, ilike/nilike return null on null inputs.
787                    bool_parts.push(BooleanArray::new_null(cs.row_len));
788                }
789                continue;
790            }
791        };
792
793        let seg_sa = seg.as_any().downcast_ref::<StringArray>()?;
794        let mut m = match text_kind {
795            0 => ilike(seg_sa, &pat).ok()?,
796            1 => nilike(seg_sa, &pat).ok()?,
797            2 => ilike(seg_sa, &pat).ok()?,
798            _ => return None,
799        };
800
801        if text_kind == 0 && empty_special {
802            // Treat nulls as equal to empty string
803            let mut bb = BooleanBuilder::with_capacity(seg_sa.len());
804            for i in 0..seg_sa.len() {
805                bb.append_value(seg_sa.is_null(i));
806            }
807            let nulls = bb.finish();
808            m = boolean::or_kleene(&m, &nulls).ok()?;
809        }
810
811        bool_parts.push(m);
812    }
813
814    if bool_parts.is_empty() {
815        None
816    } else if bool_parts.len() == 1 {
817        Some(std::sync::Arc::new(bool_parts.remove(0)))
818    } else {
819        let anys: Vec<&dyn arrow_array::Array> = bool_parts
820            .iter()
821            .map(|a| a as &dyn arrow_array::Array)
822            .collect();
823        let conc: ArrayRef = concat_arrays(&anys).ok()?;
824        let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
825        Some(std::sync::Arc::new(ba))
826    }
827}
828
829#[derive(Debug, Clone)]
830pub struct LayerInfo {
831    pub vertex_count: usize,
832    pub parallel_eligible: bool,
833    pub sample_cells: Vec<String>, // Sample of up to 5 cell addresses
834}
835
836#[derive(Debug, Clone)]
837pub struct EvalPlan {
838    pub total_vertices_to_evaluate: usize,
839    pub layers: Vec<LayerInfo>,
840    pub cycles_detected: usize,
841    pub dirty_count: usize,
842    pub volatile_count: usize,
843    pub parallel_enabled: bool,
844    pub estimated_parallel_layers: usize,
845    pub target_cells: Vec<String>,
846}
847
848impl<R> Engine<R>
849where
850    R: EvaluationContext,
851{
852    pub fn new(resolver: R, config: EvalConfig) -> Self {
853        crate::builtins::load_builtins();
854
855        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
856            #[cfg(feature = "system-clock")]
857            {
858                Arc::new(crate::timezone::SystemClock::new(
859                    crate::timezone::TimeZoneSpec::default(),
860                ))
861            }
862            #[cfg(not(feature = "system-clock"))]
863            {
864                Arc::new(crate::timezone::FixedClock::new(
865                    chrono::DateTime::UNIX_EPOCH,
866                    crate::timezone::TimeZoneSpec::Utc,
867                ))
868            }
869        });
870
871        // Initialize thread pool based on config
872        let thread_pool = if config.enable_parallel {
873            let mut builder = ThreadPoolBuilder::new();
874            if let Some(max_threads) = config.max_threads {
875                builder = builder.num_threads(max_threads);
876            }
877
878            match builder.build() {
879                Ok(pool) => Some(Arc::new(pool)),
880                Err(_) => {
881                    // Fall back to sequential evaluation if thread pool creation fails
882                    None
883                }
884            }
885        } else {
886            None
887        };
888
889        let mut engine = Self {
890            graph: DependencyGraph::new_with_config(config.clone()),
891            resolver,
892            config,
893            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
894            clock,
895            thread_pool,
896            recalc_epoch: 0,
897            snapshot_id: std::sync::atomic::AtomicU64::new(1),
898            topology_epoch: 0,
899            cached_static_schedule: None,
900            spill_mgr: ShimSpillManager::default(),
901            arrow_sheets: SheetStore::default(),
902            has_edited: false,
903            overlay_compactions: 0,
904            computed_overlay_bytes_estimate: 0,
905            computed_overlay_mirroring_disabled: false,
906            force_materialize_range_views: false,
907            row_bounds_cache: std::sync::RwLock::new(None),
908            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
909            staged_formulas: std::collections::HashMap::new(),
910            row_visibility: FxHashMap::default(),
911            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
912            formula_parse_diagnostics: Vec::new(),
913            active_cancel_flag: None,
914            action_depth: 0,
915            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
916            virtual_dep_fallback_activations: 0,
917        };
918        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
919        engine.config.arrow_storage_enabled = true;
920        engine.config.delta_overlay_enabled = true;
921        engine.config.write_formula_overlay_enabled = true;
922        let default_sheet = engine.graph.default_sheet_name().to_string();
923        engine.ensure_arrow_sheet(&default_sheet);
924        engine
925    }
926
927    /// Create an Engine with a custom thread pool (for shared thread pool scenarios)
928    pub fn with_thread_pool(
929        resolver: R,
930        config: EvalConfig,
931        thread_pool: Arc<rayon::ThreadPool>,
932    ) -> Self {
933        crate::builtins::load_builtins();
934        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
935            #[cfg(feature = "system-clock")]
936            {
937                Arc::new(crate::timezone::SystemClock::new(
938                    crate::timezone::TimeZoneSpec::default(),
939                ))
940            }
941            #[cfg(not(feature = "system-clock"))]
942            {
943                Arc::new(crate::timezone::FixedClock::new(
944                    chrono::DateTime::UNIX_EPOCH,
945                    crate::timezone::TimeZoneSpec::Utc,
946                ))
947            }
948        });
949        let mut engine = Self {
950            graph: DependencyGraph::new_with_config(config.clone()),
951            resolver,
952            config,
953            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
954            clock,
955            thread_pool: Some(thread_pool),
956            recalc_epoch: 0,
957            snapshot_id: std::sync::atomic::AtomicU64::new(1),
958            topology_epoch: 0,
959            cached_static_schedule: None,
960            spill_mgr: ShimSpillManager::default(),
961            arrow_sheets: SheetStore::default(),
962            has_edited: false,
963            overlay_compactions: 0,
964            computed_overlay_bytes_estimate: 0,
965            computed_overlay_mirroring_disabled: false,
966            force_materialize_range_views: false,
967            row_bounds_cache: std::sync::RwLock::new(None),
968            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
969            staged_formulas: std::collections::HashMap::new(),
970            row_visibility: FxHashMap::default(),
971            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
972            formula_parse_diagnostics: Vec::new(),
973            active_cancel_flag: None,
974            action_depth: 0,
975            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
976            virtual_dep_fallback_activations: 0,
977        };
978        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
979        engine.config.arrow_storage_enabled = true;
980        engine.config.delta_overlay_enabled = true;
981        engine.config.write_formula_overlay_enabled = true;
982        let default_sheet = engine.graph.default_sheet_name().to_string();
983        engine.ensure_arrow_sheet(&default_sheet);
984        engine
985    }
986
987    pub fn workbook_load_limits(&self) -> &crate::engine::WorkbookLoadLimits {
988        &self.workbook_load_limits
989    }
990
991    pub fn set_workbook_load_limits(&mut self, limits: crate::engine::WorkbookLoadLimits) {
992        self.workbook_load_limits = limits;
993    }
994
995    fn clear_source_cache(&self) {
996        if let Ok(mut g) = self.source_cache.write() {
997            *g = SourceCache::default();
998        }
999    }
1000
1001    pub fn last_virtual_dep_telemetry(&self) -> &VirtualDepTelemetry {
1002        &self.last_virtual_dep_telemetry
1003    }
1004
1005    pub fn virtual_dep_fallback_activations(&self) -> u64 {
1006        self.virtual_dep_fallback_activations
1007    }
1008
1009    fn reset_virtual_dep_telemetry_if_disabled(&mut self) {
1010        if !self.config.enable_virtual_dep_telemetry {
1011            self.last_virtual_dep_telemetry = VirtualDepTelemetry {
1012                fallback_mode_activations: self.virtual_dep_fallback_activations,
1013                ..VirtualDepTelemetry::default()
1014            };
1015        }
1016    }
1017
1018    fn source_cache_session(&self) -> SourceCacheSession {
1019        self.clear_source_cache();
1020        SourceCacheSession {
1021            cache: self.source_cache.clone(),
1022        }
1023    }
1024
1025    fn resolve_source_scalar_cached(
1026        &self,
1027        name: &str,
1028        version: Option<u64>,
1029    ) -> Result<LiteralValue, ExcelError> {
1030        let key = (name.to_string(), version);
1031        if let Ok(mut g) = self.source_cache.write() {
1032            if let Some(v) = g.scalars.get(&key) {
1033                return Ok(v.clone());
1034            }
1035
1036            let v = self.resolver.resolve_source_scalar(name).map_err(|err| {
1037                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
1038                    ExcelError::new(ExcelErrorKind::Ref)
1039                        .with_message(format!("Unresolved source scalar: {name}"))
1040                } else {
1041                    err
1042                }
1043            })?;
1044            g.scalars.insert(key, v.clone());
1045            Ok(v)
1046        } else {
1047            self.resolver.resolve_source_scalar(name).map_err(|err| {
1048                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
1049                    ExcelError::new(ExcelErrorKind::Ref)
1050                        .with_message(format!("Unresolved source scalar: {name}"))
1051                } else {
1052                    err
1053                }
1054            })
1055        }
1056    }
1057
1058    fn resolve_source_table_cached(
1059        &self,
1060        name: &str,
1061        version: Option<u64>,
1062    ) -> Result<Arc<dyn crate::traits::Table>, ExcelError> {
1063        let key = (name.to_string(), version);
1064        if let Ok(mut g) = self.source_cache.write() {
1065            if let Some(t) = g.tables.get(&key) {
1066                return Ok(t.clone());
1067            }
1068
1069            let t = self.resolver.resolve_source_table(name).map_err(|err| {
1070                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
1071                    ExcelError::new(ExcelErrorKind::Ref)
1072                        .with_message(format!("Unresolved source table: {name}"))
1073                } else {
1074                    err
1075                }
1076            })?;
1077            let t: Arc<dyn crate::traits::Table> = Arc::from(t);
1078            g.tables.insert(key, t.clone());
1079            Ok(t)
1080        } else {
1081            self.resolver
1082                .resolve_source_table(name)
1083                .map_err(|err| {
1084                    if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
1085                        ExcelError::new(ExcelErrorKind::Ref)
1086                            .with_message(format!("Unresolved source table: {name}"))
1087                    } else {
1088                        err
1089                    }
1090                })
1091                .map(Arc::from)
1092        }
1093    }
1094
1095    fn source_table_to_range_view(
1096        &self,
1097        table: &dyn crate::traits::Table,
1098        spec: &Option<formualizer_parse::parser::TableSpecifier>,
1099    ) -> Result<RangeView<'static>, ExcelError> {
1100        use formualizer_parse::parser::{SpecialItem, TableSpecifier};
1101
1102        let owned = match spec {
1103            Some(TableSpecifier::Column(c)) => {
1104                let c = c.trim();
1105                if c == "@" || c.contains('[') || c.contains(']') || c.contains(',') {
1106                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
1107                        "Complex structured references not yet supported".to_string(),
1108                    ));
1109                }
1110                table.get_column(c)?.materialise().into_owned()
1111            }
1112            Some(TableSpecifier::ColumnRange(start, end)) => {
1113                let cols = table.columns();
1114                let start = start.trim();
1115                let end = end.trim();
1116                let start_key = start.to_lowercase();
1117                let end_key = end.to_lowercase();
1118                let start_idx = cols.iter().position(|n| n.to_lowercase() == start_key);
1119                let end_idx = cols.iter().position(|n| n.to_lowercase() == end_key);
1120                if let (Some(mut si), Some(mut ei)) = (start_idx, end_idx) {
1121                    if si > ei {
1122                        std::mem::swap(&mut si, &mut ei);
1123                    }
1124                    let h = table.data_height();
1125                    let w = ei - si + 1;
1126                    let mut rows = vec![vec![LiteralValue::Empty; w]; h];
1127                    for (offset, ci) in (si..=ei).enumerate() {
1128                        let cname = &cols[ci];
1129                        let col_range = table.get_column(cname)?;
1130                        let (rh, _) = col_range.dimensions();
1131                        for (r, row) in rows.iter_mut().enumerate().take(h.min(rh)) {
1132                            row[offset] = col_range.get(r, 0)?;
1133                        }
1134                    }
1135                    rows
1136                } else {
1137                    return Err(ExcelError::new(ExcelErrorKind::Ref)
1138                        .with_message("Column range refers to unknown column(s)".to_string()));
1139                }
1140            }
1141            Some(TableSpecifier::SpecialItem(SpecialItem::Headers))
1142            | Some(TableSpecifier::Headers) => table
1143                .headers_row()
1144                .map(|r| r.materialise().into_owned())
1145                .unwrap_or_default(),
1146            Some(TableSpecifier::SpecialItem(SpecialItem::Totals))
1147            | Some(TableSpecifier::Totals) => table
1148                .totals_row()
1149                .map(|r| r.materialise().into_owned())
1150                .unwrap_or_default(),
1151            Some(TableSpecifier::SpecialItem(SpecialItem::Data)) | Some(TableSpecifier::Data) => {
1152                table
1153                    .data_body()
1154                    .map(|r| r.materialise().into_owned())
1155                    .unwrap_or_default()
1156            }
1157            Some(TableSpecifier::SpecialItem(SpecialItem::All)) | Some(TableSpecifier::All) => {
1158                let mut out: Vec<Vec<LiteralValue>> = Vec::new();
1159                if let Some(h) = table.headers_row() {
1160                    out.extend(h.iter_rows());
1161                }
1162                if let Some(body) = table.data_body() {
1163                    out.extend(body.iter_rows());
1164                }
1165                if let Some(tr) = table.totals_row() {
1166                    out.extend(tr.iter_rows());
1167                }
1168                out
1169            }
1170            Some(TableSpecifier::SpecialItem(SpecialItem::ThisRow)) => {
1171                return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
1172                    "@ (This Row) requires table-aware context; not yet supported".to_string(),
1173                ));
1174            }
1175            Some(TableSpecifier::Row(_)) | Some(TableSpecifier::Combination(_)) => {
1176                return Err(ExcelError::new(ExcelErrorKind::NImpl)
1177                    .with_message("Complex structured references not yet supported".to_string()));
1178            }
1179            None => {
1180                return Err(ExcelError::new(ExcelErrorKind::NImpl)
1181                    .with_message("Table reference without specifier is unsupported".to_string()));
1182            }
1183        };
1184
1185        Ok(RangeView::from_owned_rows(owned, self.config.date_system))
1186    }
1187
1188    pub fn default_sheet_id(&self) -> SheetId {
1189        self.graph.default_sheet_id()
1190    }
1191
1192    pub fn default_sheet_name(&self) -> &str {
1193        self.graph.default_sheet_name()
1194    }
1195
1196    /// Update the workbook seed for deterministic RNGs in functions.
1197    pub fn set_workbook_seed(&mut self, seed: u64) {
1198        self.config.workbook_seed = seed;
1199    }
1200
1201    /// Set the volatile level policy (Always/OnRecalc/OnOpen)
1202    pub fn set_volatile_level(&mut self, level: crate::traits::VolatileLevel) {
1203        self.config.volatile_level = level;
1204    }
1205
1206    /// Enable/disable deterministic evaluation mode (fixed clock + timezone).
1207    pub fn set_deterministic_mode(
1208        &mut self,
1209        mode: crate::engine::DeterministicMode,
1210    ) -> Result<(), ExcelError> {
1211        let clock = mode.build_clock()?;
1212        self.config.deterministic_mode = mode;
1213        self.clock = clock;
1214        Ok(())
1215    }
1216
1217    fn validate_deterministic_mode(&self) -> Result<(), ExcelError> {
1218        self.config.deterministic_mode.validate()
1219    }
1220
1221    pub fn sheet_id(&self, name: &str) -> Option<SheetId> {
1222        self.graph.sheet_id(name)
1223    }
1224
1225    pub fn sheet_id_mut(&mut self, name: &str) -> SheetId {
1226        self.add_sheet(name)
1227            .unwrap_or_else(|_| self.graph.sheet_id_mut(name))
1228    }
1229
1230    pub fn sheet_name(&self, id: SheetId) -> &str {
1231        self.graph.sheet_name(id)
1232    }
1233
1234    pub fn add_sheet(&mut self, name: &str) -> Result<SheetId, ExcelError> {
1235        let id = self.graph.add_sheet(name)?;
1236        self.ensure_arrow_sheet(name);
1237        self.mark_topology_edited();
1238        Ok(id)
1239    }
1240
1241    fn ensure_arrow_sheet(&mut self, name: &str) {
1242        if self.arrow_sheets.sheet(name).is_some() {
1243            return;
1244        }
1245        self.arrow_sheets
1246            .sheets
1247            .push(crate::arrow_store::ArrowSheet {
1248                name: std::sync::Arc::<str>::from(name),
1249                columns: Vec::new(),
1250                nrows: 0,
1251                chunk_starts: Vec::new(),
1252                chunk_rows: 32 * 1024,
1253            });
1254    }
1255
1256    pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
1257        let name = self.graph.sheet_name(sheet_id).to_string();
1258        self.graph.remove_sheet(sheet_id)?;
1259        self.arrow_sheets.sheets.retain(|s| s.name.as_ref() != name);
1260        if self.row_visibility.remove(&sheet_id).is_some() {
1261            self.invalidate_row_visibility_mask_cache();
1262        }
1263        Ok(())
1264    }
1265
1266    /// Helper to synchronize the Arrow-backed storage layer.
1267    fn rename_sheet_in_arrow_store(&mut self, target_name: &str, new_name: &str) -> bool {
1268        if let Some(asheet) = self
1269            .arrow_sheets
1270            .sheets
1271            .iter_mut()
1272            .find(|s| s.name.as_ref() == target_name)
1273        {
1274            asheet.name = std::sync::Arc::<str>::from(new_name);
1275            return true;
1276        }
1277        false
1278    }
1279
1280    pub fn rename_sheet(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
1281        let old_name = self.graph.sheet_name(sheet_id).to_string();
1282
1283        // Speculative Storage Update
1284        // Update name in storage FIRST so the Evaluator can find it during Graph rescue.
1285        self.rename_sheet_in_arrow_store(&old_name, new_name);
1286
1287        // Graph Update (Metadata + Rescue Logic)
1288        match self.graph.rename_sheet(sheet_id, new_name) {
1289            Ok(_) => {
1290                // Success! Invalidate cache for the moved sheet
1291                let sheet_vertices: Vec<VertexId> =
1292                    self.graph.vertices_in_sheet(sheet_id).collect();
1293                for v_id in sheet_vertices {
1294                    self.graph.mark_vertex_dirty(v_id);
1295                }
1296                self.mark_topology_edited();
1297                Ok(())
1298            }
1299            Err(e) => {
1300                // ROLLBACK: Revert storage if graph rejected the name
1301                self.rename_sheet_in_arrow_store(new_name, &old_name);
1302                Err(e)
1303            }
1304        }
1305    }
1306
1307    pub fn named_ranges_iter(
1308        &self,
1309    ) -> impl Iterator<Item = (&String, &crate::engine::named_range::NamedRange)> {
1310        self.graph.named_ranges_iter()
1311    }
1312
1313    pub fn sheet_named_ranges_iter(
1314        &self,
1315    ) -> impl Iterator<Item = (&(SheetId, String), &crate::engine::named_range::NamedRange)> {
1316        self.graph.sheet_named_ranges_iter()
1317    }
1318
1319    pub fn resolve_name_entry(
1320        &self,
1321        name: &str,
1322        current_sheet: SheetId,
1323    ) -> Option<&crate::engine::named_range::NamedRange> {
1324        self.graph.resolve_name_entry(name, current_sheet)
1325    }
1326
1327    pub fn named_ranges_snapshot(&self) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
1328        let mut out: Vec<crate::engine::named_range::NamedRangeSnapshot> = Vec::new();
1329
1330        for (name, named) in self.graph.named_ranges_iter() {
1331            out.push(crate::engine::named_range::NamedRangeSnapshot {
1332                name: name.clone(),
1333                scope: NameScope::Workbook,
1334                definition: named.definition.clone(),
1335            });
1336        }
1337
1338        for ((sheet_id, name), named) in self.graph.sheet_named_ranges_iter() {
1339            out.push(crate::engine::named_range::NamedRangeSnapshot {
1340                name: name.clone(),
1341                scope: NameScope::Sheet(*sheet_id),
1342                definition: named.definition.clone(),
1343            });
1344        }
1345
1346        out.sort_by(|a, b| {
1347            let a_scope = match a.scope {
1348                NameScope::Workbook => (0u8, 0u32),
1349                NameScope::Sheet(id) => (1u8, u32::from(id)),
1350            };
1351            let b_scope = match b.scope {
1352                NameScope::Workbook => (0u8, 0u32),
1353                NameScope::Sheet(id) => (1u8, u32::from(id)),
1354            };
1355            a_scope.cmp(&b_scope).then_with(|| a.name.cmp(&b.name))
1356        });
1357
1358        out
1359    }
1360
1361    pub fn named_ranges_snapshot_for_sheet(
1362        &self,
1363        sheet_id: SheetId,
1364    ) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
1365        self.named_ranges_snapshot()
1366            .into_iter()
1367            .filter(|entry| match entry.scope {
1368                NameScope::Workbook => true,
1369                NameScope::Sheet(id) => id == sheet_id,
1370            })
1371            .collect()
1372    }
1373
1374    pub fn define_name(
1375        &mut self,
1376        name: &str,
1377        definition: NamedDefinition,
1378        scope: NameScope,
1379    ) -> Result<(), ExcelError> {
1380        self.graph.define_name(name, definition, scope)?;
1381        self.mark_topology_edited();
1382        Ok(())
1383    }
1384
1385    pub fn update_name(
1386        &mut self,
1387        name: &str,
1388        definition: NamedDefinition,
1389        scope: NameScope,
1390    ) -> Result<(), ExcelError> {
1391        self.graph.update_name(name, definition, scope)?;
1392        self.mark_topology_edited();
1393        Ok(())
1394    }
1395
1396    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
1397        self.graph.delete_name(name, scope)?;
1398        self.mark_topology_edited();
1399        Ok(())
1400    }
1401
1402    pub fn define_table(
1403        &mut self,
1404        name: &str,
1405        range: crate::reference::RangeRef,
1406        header_row: bool,
1407        headers: Vec<String>,
1408        totals_row: bool,
1409    ) -> Result<(), ExcelError> {
1410        self.graph
1411            .define_table(name, range, header_row, headers, totals_row)?;
1412        self.mark_topology_edited();
1413        Ok(())
1414    }
1415
1416    pub fn define_source_scalar(
1417        &mut self,
1418        name: &str,
1419        version: Option<u64>,
1420    ) -> Result<(), ExcelError> {
1421        self.graph.define_source_scalar(name, version)?;
1422        self.mark_topology_edited();
1423        Ok(())
1424    }
1425
1426    pub fn define_source_table(
1427        &mut self,
1428        name: &str,
1429        version: Option<u64>,
1430    ) -> Result<(), ExcelError> {
1431        self.graph.define_source_table(name, version)?;
1432        self.mark_topology_edited();
1433        Ok(())
1434    }
1435
1436    pub fn set_source_scalar_version(
1437        &mut self,
1438        name: &str,
1439        version: Option<u64>,
1440    ) -> Result<(), ExcelError> {
1441        self.graph.set_source_scalar_version(name, version)
1442    }
1443
1444    pub fn set_source_table_version(
1445        &mut self,
1446        name: &str,
1447        version: Option<u64>,
1448    ) -> Result<(), ExcelError> {
1449        self.graph.set_source_table_version(name, version)
1450    }
1451
1452    pub fn invalidate_source(&mut self, name: &str) -> Result<(), ExcelError> {
1453        self.graph.invalidate_source(name)
1454    }
1455
1456    pub fn vertex_value(&self, vertex: VertexId) -> Option<LiteralValue> {
1457        self.graph.get_value(vertex)
1458    }
1459
1460    pub fn graph_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
1461        self.graph.get_cell_value(sheet, row, col)
1462    }
1463
1464    pub fn vertex_for_cell(&self, cell: &CellRef) -> Option<VertexId> {
1465        self.graph.get_vertex_for_cell(cell)
1466    }
1467
1468    pub fn evaluation_vertices(&self) -> Vec<VertexId> {
1469        self.graph.get_evaluation_vertices()
1470    }
1471
1472    pub fn set_first_load_assume_new(&mut self, enabled: bool) {
1473        self.graph.set_first_load_assume_new(enabled);
1474    }
1475
1476    pub fn reset_ensure_touched(&mut self) {
1477        self.graph.reset_ensure_touched();
1478    }
1479
1480    pub fn finalize_sheet_index(&mut self, sheet: &str) {
1481        self.graph.finalize_sheet_index(sheet);
1482    }
1483
1484    /// Execute a named Engine action.
1485    ///
1486    /// Ticket 614 introduces this as the stable Engine-level transaction surface.
1487    /// For now actions are commit-only: they do not create changelog boundaries and they do not
1488    /// provide rollback/atomicity.
1489    ///
1490    /// Nested actions are deterministically handled by *disallowing* nesting: calling
1491    /// `Engine::action` while another action is active returns `EditorError::TransactionFailed`.
1492    pub fn action<T>(
1493        &mut self,
1494        name: impl AsRef<str>,
1495        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
1496    ) -> Result<T, crate::engine::EditorError> {
1497        if self.action_depth != 0 {
1498            return Err(crate::engine::EditorError::TransactionFailed {
1499                reason: "Nested Engine::action calls are not supported (ticket 614: commit-only surface)"
1500                    .to_string(),
1501            });
1502        }
1503
1504        self.action_depth = 1;
1505        let engine_ptr: *mut Engine<R> = self;
1506        let _guard = ActionDepthGuard {
1507            engine: engine_ptr,
1508            _marker: std::marker::PhantomData,
1509        };
1510
1511        let mut tx = EngineAction {
1512            engine: self,
1513            name: name.as_ref().to_string(),
1514            log: None,
1515            arrow_undo: None,
1516            atomic_policy: false,
1517        };
1518        f(&mut tx)
1519    }
1520
1521    /// Execute a named Engine action with atomic commit/rollback semantics.
1522    ///
1523    /// This variant does not require a `ChangeLog` and uses an internal journal for rollback.
1524    pub fn action_atomic<T>(
1525        &mut self,
1526        name: impl Into<String>,
1527        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
1528    ) -> Result<T, crate::engine::EditorError> {
1529        let (v, _j) = self.action_atomic_journal(name, f)?;
1530        Ok(v)
1531    }
1532
1533    /// Like `action_atomic`, but returns the committed journal entry for undo/redo storage.
1534    pub fn action_atomic_journal<T>(
1535        &mut self,
1536        name: impl Into<String>,
1537        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
1538    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
1539        if self.action_depth != 0 {
1540            return Err(crate::engine::EditorError::TransactionFailed {
1541                reason: "Nested Engine::action calls are not supported (deterministic rule)"
1542                    .to_string(),
1543            });
1544        }
1545
1546        self.action_depth = 1;
1547        let engine_ptr: *mut Engine<R> = self;
1548        let _guard = ActionDepthGuard {
1549            engine: engine_ptr,
1550            _marker: std::marker::PhantomData,
1551        };
1552
1553        let name_str = name.into();
1554        let mut log = crate::engine::ChangeLog::new();
1555        let start_len = log.len();
1556        self.action_atomic_impl(&mut log, start_len, name_str, f)
1557    }
1558
1559    fn action_atomic_impl<T>(
1560        &mut self,
1561        log: &mut crate::engine::ChangeLog,
1562        start_len: usize,
1563        name: String,
1564        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
1565    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
1566        let mut arrow_undo = crate::engine::ArrowUndoBatch::default();
1567        let arrow_ptr: *mut crate::engine::ArrowUndoBatch = &mut arrow_undo;
1568
1569        let log_ptr: *mut crate::engine::ChangeLog = log;
1570        let mut tx = EngineAction {
1571            engine: self,
1572            name: name.clone(),
1573            log: Some(log_ptr),
1574            arrow_undo: Some(arrow_ptr),
1575            atomic_policy: true,
1576        };
1577
1578        let res = f(&mut tx);
1579
1580        // Capture graph structural delta for this action.
1581        let graph_events: Vec<crate::engine::ChangeEvent> =
1582            unsafe { (&*log_ptr).events() }[start_len..].to_vec();
1583        let graph_batch = crate::engine::GraphUndoBatch {
1584            events: graph_events,
1585        };
1586        let affected_cells = arrow_undo.ops.len();
1587        let journal = crate::engine::ActionJournal {
1588            name,
1589            graph: graph_batch,
1590            arrow: arrow_undo,
1591            affected_cells,
1592        };
1593
1594        match res {
1595            Ok(v) => {
1596                if !journal.graph.is_empty() || !journal.arrow.is_empty() {
1597                    self.mark_data_edited();
1598                }
1599                Ok((v, journal))
1600            }
1601            Err(e) => {
1602                if let Err(rb) = self.rollback_from_action_journal(&journal) {
1603                    return Err(crate::engine::EditorError::TransactionFailed {
1604                        reason: format!(
1605                            "Engine::action_atomic rollback failed after error '{e}': {rb}"
1606                        ),
1607                    });
1608                }
1609                Err(e)
1610            }
1611        }
1612    }
1613
1614    /// Execute a named Engine action, logging graph changes into the provided ChangeLog.
1615    ///
1616    /// Ticket 615: this variant provides atomicity. If the action returns an error, it rolls back:
1617    /// - Dependency graph structural edits (via inverse ChangeEvents)
1618    /// - Arrow-truth overlay writes mirrored from ChangeEvents
1619    /// - ChangeLog entries (truncated back to the pre-action length)
1620    pub fn action_with_logger<T>(
1621        &mut self,
1622        log: &mut crate::engine::ChangeLog,
1623        name: impl AsRef<str>,
1624        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
1625    ) -> Result<T, crate::engine::EditorError> {
1626        if self.action_depth != 0 {
1627            return Err(crate::engine::EditorError::TransactionFailed {
1628                reason: "Nested Engine::action calls are not supported (deterministic rule)"
1629                    .to_string(),
1630            });
1631        }
1632
1633        self.action_depth = 1;
1634        let engine_ptr: *mut Engine<R> = self;
1635        let _guard = ActionDepthGuard {
1636            engine: engine_ptr,
1637            _marker: std::marker::PhantomData,
1638        };
1639
1640        let start_len = log.len();
1641        let name_str = name.as_ref().to_string();
1642        log.begin_compound(name_str.clone());
1643
1644        // Use the provided ChangeLog as an observability sink.
1645        // Correctness is provided by the internal `ActionJournal` returned from the atomic impl.
1646        let res = self.action_atomic_impl(log, start_len, name_str, f);
1647
1648        match res {
1649            Ok((v, _journal)) => {
1650                log.end_compound();
1651                Ok(v)
1652            }
1653            Err(e) => {
1654                // Close compound and truncate log as cleanup only.
1655                log.end_compound();
1656                log.truncate(start_len);
1657                Err(e)
1658            }
1659        }
1660    }
1661
1662    fn rollback_from_action_journal(
1663        &mut self,
1664        journal: &crate::engine::ActionJournal,
1665    ) -> Result<(), crate::engine::EditorError> {
1666        // 1) Roll back the dependency graph structure.
1667        journal.graph.undo(&mut self.graph)?;
1668        // 2) Roll back engine row-visibility sidecar events.
1669        self.apply_inverse_row_visibility_events(&journal.graph.events);
1670        // 3) Roll back Arrow-truth overlays.
1671        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
1672        Ok(())
1673    }
1674
1675    fn rollback_from_change_events(
1676        &mut self,
1677        events: &[crate::engine::ChangeEvent],
1678    ) -> Result<(), crate::engine::EditorError> {
1679        use crate::engine::ChangeEvent;
1680
1681        // 1) Roll back the dependency graph.
1682        {
1683            let mut editor = crate::engine::VertexEditor::new(&mut self.graph);
1684            let mut compound_stack: Vec<usize> = Vec::new();
1685            for ev in events.iter().rev() {
1686                match ev {
1687                    ChangeEvent::CompoundEnd { depth } => compound_stack.push(*depth),
1688                    ChangeEvent::CompoundStart { depth, .. } => {
1689                        if compound_stack.last() == Some(depth) {
1690                            compound_stack.pop();
1691                        }
1692                    }
1693                    ChangeEvent::SetRowVisibility { .. } => {
1694                        // Engine-side metadata handled after dropping graph editor borrow.
1695                    }
1696                    _ => {
1697                        editor.apply_inverse(ev.clone())?;
1698                    }
1699                }
1700            }
1701        }
1702
1703        // 2) Roll back engine row-visibility metadata.
1704        for ev in events.iter().rev() {
1705            self.apply_inverse_row_visibility_event(ev);
1706        }
1707
1708        // 3) Roll back Arrow-truth overlays mirrored from those ChangeEvents.
1709        for ev in events.iter().rev() {
1710            self.mirror_inverse_change_to_arrow(ev);
1711        }
1712
1713        Ok(())
1714    }
1715
1716    fn read_cell_formula_ast(&self, sheet: &str, row: u32, col: u32) -> Option<ASTNode> {
1717        let sheet_id = self.graph.sheet_id(sheet)?;
1718        let coord = Coord::from_excel(row, col, true, true);
1719        let cell = CellRef::new(sheet_id, coord);
1720        let vid = self.graph.get_vertex_for_cell(&cell)?;
1721        let ast_id = self.graph.get_formula_id(vid)?;
1722        self.graph
1723            .data_store()
1724            .retrieve_ast(ast_id, self.graph.sheet_reg())
1725    }
1726
1727    pub fn edit_with_logger<T>(
1728        &mut self,
1729        log: &mut crate::engine::ChangeLog,
1730        f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
1731    ) -> T {
1732        // Record starting log length so we can mirror only newly-recorded events.
1733        let start_len = log.len();
1734
1735        // Provide a spill snapshot reader so VertexEditor can snapshot Arrow-truth spill values
1736        // (graph value cache is intentionally empty in canonical mode).
1737        struct ArrowSpillReader<'a> {
1738            sheets: &'a crate::arrow_store::SheetStore,
1739        }
1740        impl crate::engine::graph::editor::vertex_editor::SpillValueReader for ArrowSpillReader<'_> {
1741            fn read_cell_value(
1742                &self,
1743                sheet: &str,
1744                row: u32,
1745                col: u32,
1746            ) -> Option<formualizer_common::LiteralValue> {
1747                use formualizer_common::LiteralValue;
1748                let asheet = self.sheets.sheet(sheet)?;
1749                let r0 = row.saturating_sub(1) as usize;
1750                let c0 = col.saturating_sub(1) as usize;
1751                let v = asheet.get_cell_value(r0, c0);
1752                if matches!(v, LiteralValue::Empty) {
1753                    None
1754                } else {
1755                    Some(v)
1756                }
1757            }
1758        }
1759
1760        let ret = {
1761            let spill_reader = ArrowSpillReader {
1762                sheets: &self.arrow_sheets,
1763            };
1764            let mut editor = crate::engine::VertexEditor::with_logger_and_spill_reader(
1765                &mut self.graph,
1766                log,
1767                &spill_reader,
1768            );
1769            f(&mut editor)
1770        };
1771
1772        // Mirror value-impacting graph events to Arrow for forward edits.
1773        // This keeps Arrow overlays (delta + computed) consistent when edits clear/commit spills.
1774        for ev in &log.events()[start_len..] {
1775            self.mirror_forward_change_to_arrow(ev);
1776        }
1777
1778        ret
1779    }
1780
1781    pub fn undo_logged(
1782        &mut self,
1783        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
1784        log: &mut crate::engine::ChangeLog,
1785    ) -> Result<(), crate::engine::EditorError> {
1786        let batch = undo.undo(&mut self.graph, log)?;
1787        for item in batch.iter().rev() {
1788            self.apply_inverse_row_visibility_event(&item.event);
1789        }
1790        self.mirror_undo_batch_to_arrow(&batch);
1791        Ok(())
1792    }
1793
1794    pub fn redo_logged(
1795        &mut self,
1796        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
1797        log: &mut crate::engine::ChangeLog,
1798    ) -> Result<(), crate::engine::EditorError> {
1799        let batch = undo.redo(&mut self.graph, log)?;
1800        for item in &batch {
1801            self.apply_forward_row_visibility_event(&item.event);
1802        }
1803        self.mirror_redo_batch_to_arrow(&batch);
1804        Ok(())
1805    }
1806
1807    /// Undo the last committed atomic action using the journal stack.
1808    ///
1809    /// This path does not require a `ChangeLog`.
1810    pub fn undo_action(
1811        &mut self,
1812        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
1813    ) -> Result<(), crate::engine::EditorError> {
1814        let Some(journal) = undo.pop_undo_action() else {
1815            return Ok(());
1816        };
1817
1818        journal.graph.undo(&mut self.graph)?;
1819        self.apply_inverse_row_visibility_events(&journal.graph.events);
1820        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
1821
1822        undo.push_redo_action(journal);
1823        Ok(())
1824    }
1825
1826    /// Redo the last undone atomic action using the journal stack.
1827    ///
1828    /// This path does not require a `ChangeLog`.
1829    pub fn redo_action(
1830        &mut self,
1831        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
1832    ) -> Result<(), crate::engine::EditorError> {
1833        let Some(journal) = undo.pop_redo_action() else {
1834            return Ok(());
1835        };
1836
1837        journal.graph.redo(&mut self.graph)?;
1838        self.apply_forward_row_visibility_events(&journal.graph.events);
1839        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ false);
1840
1841        undo.push_done_action(journal);
1842        Ok(())
1843    }
1844
1845    fn cellref_to_sheet_row_col(&self, addr: &crate::reference::CellRef) -> (String, u32, u32) {
1846        let sheet = self.graph.sheet_name(addr.sheet_id).to_string();
1847        // Coord stores 0-based indices.
1848        let row = addr.coord.row() + 1;
1849        let col = addr.coord.col() + 1;
1850        (sheet, row, col)
1851    }
1852
1853    fn mirror_undo_batch_to_arrow(
1854        &mut self,
1855        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
1856    ) {
1857        // Undo applies inverses in reverse order.
1858        for item in batch.iter().rev() {
1859            self.mirror_inverse_change_to_arrow(&item.event);
1860        }
1861    }
1862
1863    fn mirror_redo_batch_to_arrow(
1864        &mut self,
1865        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
1866    ) {
1867        // Redo applies events in forward order.
1868        for item in batch.iter() {
1869            self.mirror_forward_change_to_arrow(&item.event);
1870        }
1871    }
1872
1873    fn mirror_inverse_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
1874        use crate::engine::ChangeEvent;
1875        use formualizer_common::LiteralValue;
1876
1877        match ev {
1878            ChangeEvent::SetValue {
1879                addr,
1880                old_value,
1881                old_formula,
1882                ..
1883            } => {
1884                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
1885                if old_formula.is_some() {
1886                    self.clear_delta_overlay_cell(&sheet, row, col);
1887                } else {
1888                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
1889                    self.mirror_value_to_overlay(&sheet, row, col, &v);
1890                }
1891            }
1892            ChangeEvent::SetFormula {
1893                addr,
1894                old_value,
1895                old_formula,
1896                ..
1897            } => {
1898                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
1899                if old_formula.is_some() {
1900                    self.clear_delta_overlay_cell(&sheet, row, col);
1901                } else {
1902                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
1903                    self.mirror_value_to_overlay(&sheet, row, col, &v);
1904                }
1905            }
1906            ChangeEvent::SpillCommitted { old, new, .. } => {
1907                // Inverse: restore `old` (or clear if none).
1908                self.mirror_spill_snapshot(new, /*clear_only=*/ true);
1909                if let Some(snap) = old {
1910                    self.mirror_spill_snapshot(snap, /*clear_only=*/ false);
1911                }
1912            }
1913            ChangeEvent::SpillCleared { old, .. } => {
1914                // Inverse: restore prior spill.
1915                self.mirror_spill_snapshot(old, /*clear_only=*/ false);
1916            }
1917            ChangeEvent::SetRowVisibility { .. } => {
1918                // Engine-side metadata only; no Arrow overlay effect.
1919            }
1920            _ => {}
1921        }
1922    }
1923
1924    fn mirror_forward_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
1925        use crate::engine::ChangeEvent;
1926
1927        match ev {
1928            ChangeEvent::SetValue { addr, new, .. } => {
1929                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
1930                self.mirror_value_to_overlay(&sheet, row, col, new);
1931            }
1932            ChangeEvent::SetFormula { addr, .. } => {
1933                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
1934                self.clear_delta_overlay_cell(&sheet, row, col);
1935                // Keep any computed overlay for this cell as-is; it will be recomputed on demand.
1936            }
1937            ChangeEvent::SpillCommitted { old, new, .. } => {
1938                if let Some(snap) = old {
1939                    self.mirror_spill_snapshot(snap, /*clear_only=*/ true);
1940                }
1941                self.mirror_spill_snapshot(new, /*clear_only=*/ false);
1942            }
1943            ChangeEvent::SpillCleared { old, .. } => {
1944                self.mirror_spill_snapshot(old, /*clear_only=*/ true);
1945            }
1946            ChangeEvent::SetRowVisibility { .. } => {
1947                // Engine-side metadata only; no Arrow overlay effect.
1948            }
1949            _ => {
1950                // Other graph structural operations do not have direct value effects in Arrow.
1951            }
1952        }
1953    }
1954
1955    fn mirror_spill_snapshot(
1956        &mut self,
1957        snap: &crate::engine::graph::editor::change_log::SpillSnapshot,
1958        clear_only: bool,
1959    ) {
1960        use formualizer_common::LiteralValue;
1961
1962        let mut i = 0usize;
1963        for row in &snap.values {
1964            for v in row {
1965                if let Some(cell) = snap.target_cells.get(i) {
1966                    let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
1967                    let out = if clear_only {
1968                        LiteralValue::Empty
1969                    } else {
1970                        v.clone()
1971                    };
1972                    self.mirror_value_to_computed_overlay(&sheet, r, c, &out);
1973                }
1974                i += 1;
1975            }
1976        }
1977        // If target_cells is longer than values (should not happen), clear remaining cells.
1978        if clear_only {
1979            for cell in snap.target_cells.iter().skip(i) {
1980                let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
1981                self.mirror_value_to_computed_overlay(&sheet, r, c, &LiteralValue::Empty);
1982            }
1983        }
1984    }
1985
1986    pub fn set_default_sheet_by_name(&mut self, name: &str) {
1987        self.graph.set_default_sheet_by_name(name);
1988    }
1989
1990    pub fn set_default_sheet_by_id(&mut self, id: SheetId) {
1991        self.graph.set_default_sheet_by_id(id);
1992    }
1993
1994    pub fn set_sheet_index_mode(&mut self, mode: crate::engine::SheetIndexMode) {
1995        self.graph.set_sheet_index_mode(mode);
1996    }
1997
1998    fn clear_cached_static_schedule(&mut self) {
1999        self.cached_static_schedule = None;
2000    }
2001
2002    /// Mark data edited: bump snapshot and set edited flag.
2003    /// Value-only edits keep the stable-topology schedule cache alive.
2004    pub fn mark_data_edited(&mut self) {
2005        self.snapshot_id
2006            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2007        self.has_edited = true;
2008    }
2009
2010    /// Mark a topology-changing edit: bump snapshot + topology epoch and invalidate cached schedules.
2011    pub fn mark_topology_edited(&mut self) {
2012        self.snapshot_id
2013            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2014        self.topology_epoch = self.topology_epoch.wrapping_add(1);
2015        self.clear_cached_static_schedule();
2016        self.has_edited = true;
2017    }
2018
2019    /// Access Arrow sheet store (read-only)
2020    pub fn sheet_store(&self) -> &SheetStore {
2021        &self.arrow_sheets
2022    }
2023
2024    /// Access Arrow sheet store (mutable)
2025    pub fn sheet_store_mut(&mut self) -> &mut SheetStore {
2026        &mut self.arrow_sheets
2027    }
2028
2029    /// Stage a formula text instead of inserting into the graph (used when deferring is enabled).
2030    pub fn stage_formula_text(&mut self, sheet: &str, row: u32, col: u32, text: String) {
2031        self.staged_formulas
2032            .entry(sheet.to_string())
2033            .or_default()
2034            .push((row, col, text));
2035    }
2036
2037    /// Get a staged formula text for a given cell if present (cloned).
2038    pub fn get_staged_formula_text(&self, sheet: &str, row: u32, col: u32) -> Option<String> {
2039        self.staged_formulas.get(sheet).and_then(|v| {
2040            v.iter()
2041                .find(|(r, c, _)| *r == row && *c == col)
2042                .map(|(_, _, s)| s.clone())
2043        })
2044    }
2045
2046    pub fn formula_parse_diagnostics(&self) -> &[FormulaParseDiagnostic] {
2047        &self.formula_parse_diagnostics
2048    }
2049
2050    pub fn take_formula_parse_diagnostics(&mut self) -> Vec<FormulaParseDiagnostic> {
2051        std::mem::take(&mut self.formula_parse_diagnostics)
2052    }
2053
2054    pub fn clear_formula_parse_diagnostics(&mut self) {
2055        self.formula_parse_diagnostics.clear();
2056    }
2057
2058    pub fn handle_formula_parse_error(
2059        &mut self,
2060        sheet: &str,
2061        row: u32,
2062        col: u32,
2063        formula: &str,
2064        message: String,
2065    ) -> Result<Option<ASTNode>, ExcelError> {
2066        let policy = self.config.formula_parse_policy;
2067
2068        if policy == FormulaParsePolicy::Strict {
2069            let col_a1 = col_letters_from_1based(col).unwrap_or_else(|_| "?".to_string());
2070            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
2071                "Formula parse error at {sheet}!{col_a1}{row}: {message}"
2072            )));
2073        }
2074
2075        self.formula_parse_diagnostics.push(FormulaParseDiagnostic {
2076            sheet: sheet.to_string(),
2077            row,
2078            col,
2079            formula: formula.to_string(),
2080            message: message.clone(),
2081            policy,
2082        });
2083
2084        match policy {
2085            FormulaParsePolicy::Strict => unreachable!(),
2086            FormulaParsePolicy::KeepCachedValue => Ok(None),
2087            FormulaParsePolicy::AsText => Ok(Some(ASTNode::new(
2088                ASTNodeType::Literal(LiteralValue::Text(formula.to_string())),
2089                None,
2090            ))),
2091            FormulaParsePolicy::CoerceToError => {
2092                let err = ExcelError::new(ExcelErrorKind::Error)
2093                    .with_message(format!("Malformed formula: {message}"));
2094                Ok(Some(ASTNode::new(
2095                    ASTNodeType::Literal(LiteralValue::Error(err)),
2096                    None,
2097                )))
2098            }
2099        }
2100    }
2101
2102    /// Build graph for all staged formulas.
2103    pub fn build_graph_all(&mut self) -> Result<(), formualizer_parse::ExcelError> {
2104        if self.staged_formulas.is_empty() {
2105            return Ok(());
2106        }
2107        // Take staged formulas before borrowing graph via builder.
2108        let staged = std::mem::take(&mut self.staged_formulas);
2109        for sheet in staged.keys() {
2110            let _ = self.add_sheet(sheet);
2111        }
2112
2113        // Parse/recover first, then borrow graph builder.
2114        let mut prepared: PreparedFormulaBatches = Vec::new();
2115        for (sheet, entries) in staged {
2116            let mut formulas: Vec<ParsedFormulaEntry> = Vec::new();
2117            let mut cache: rustc_hash::FxHashMap<String, ASTNode> =
2118                rustc_hash::FxHashMap::default();
2119            cache.reserve(4096);
2120
2121            for (row, col, txt) in entries {
2122                let key = if txt.starts_with('=') {
2123                    txt
2124                } else {
2125                    format!("={txt}")
2126                };
2127                let ast = if let Some(p) = cache.get(&key) {
2128                    Some(p.clone())
2129                } else {
2130                    match formualizer_parse::parser::parse(&key) {
2131                        Ok(parsed) => {
2132                            cache.insert(key.clone(), parsed.clone());
2133                            Some(parsed)
2134                        }
2135                        Err(e) => {
2136                            self.handle_formula_parse_error(&sheet, row, col, &key, e.to_string())?
2137                        }
2138                    }
2139                };
2140
2141                if let Some(ast) = ast {
2142                    formulas.push((row, col, ast));
2143                }
2144            }
2145
2146            if !formulas.is_empty() {
2147                prepared.push((sheet, formulas));
2148            }
2149        }
2150
2151        if !prepared.is_empty() {
2152            let mut builder = self.begin_bulk_ingest();
2153            for (sheet, formulas) in prepared {
2154                let sid = builder.add_sheet(&sheet);
2155                builder.add_formulas(sid, formulas.into_iter());
2156            }
2157            let _ = builder.finish();
2158        }
2159        Ok(())
2160    }
2161
2162    /// Build graph for specific sheets (consuming only those staged entries).
2163    pub fn build_graph_for_sheets<'a, I: IntoIterator<Item = &'a str>>(
2164        &mut self,
2165        sheets: I,
2166    ) -> Result<(), formualizer_parse::ExcelError> {
2167        let mut collected: StagedFormulaBatches = Vec::new();
2168        for s in sheets {
2169            if let Some(entries) = self.staged_formulas.remove(s) {
2170                collected.push((s.to_string(), entries));
2171            }
2172        }
2173
2174        if collected.is_empty() {
2175            return Ok(());
2176        }
2177
2178        for (sheet, _) in &collected {
2179            let _ = self.add_sheet(sheet);
2180        }
2181
2182        // Parse/recover first, then borrow graph builder.
2183        let mut prepared: PreparedFormulaBatches = Vec::new();
2184        let mut cache: rustc_hash::FxHashMap<String, ASTNode> = rustc_hash::FxHashMap::default();
2185        cache.reserve(4096);
2186
2187        for (sheet, entries) in collected {
2188            let mut formulas: Vec<ParsedFormulaEntry> = Vec::new();
2189            for (row, col, txt) in entries {
2190                let key = if txt.starts_with('=') {
2191                    txt
2192                } else {
2193                    format!("={txt}")
2194                };
2195                let ast = if let Some(p) = cache.get(&key) {
2196                    Some(p.clone())
2197                } else {
2198                    match formualizer_parse::parser::parse(&key) {
2199                        Ok(parsed) => {
2200                            cache.insert(key.clone(), parsed.clone());
2201                            Some(parsed)
2202                        }
2203                        Err(e) => {
2204                            self.handle_formula_parse_error(&sheet, row, col, &key, e.to_string())?
2205                        }
2206                    }
2207                };
2208
2209                if let Some(ast) = ast {
2210                    formulas.push((row, col, ast));
2211                }
2212            }
2213            if !formulas.is_empty() {
2214                prepared.push((sheet, formulas));
2215            }
2216        }
2217
2218        if !prepared.is_empty() {
2219            let mut builder = self.begin_bulk_ingest();
2220            for (sheet, formulas) in prepared {
2221                let sid = builder.add_sheet(&sheet);
2222                builder.add_formulas(sid, formulas.into_iter());
2223            }
2224            let _ = builder.finish();
2225        }
2226        Ok(())
2227    }
2228
2229    /// Begin bulk Arrow ingest for base values (Phase A)
2230    pub fn begin_bulk_ingest_arrow(
2231        &mut self,
2232    ) -> crate::engine::arrow_ingest::ArrowBulkIngestBuilder<'_, R> {
2233        crate::engine::arrow_ingest::ArrowBulkIngestBuilder::new(self)
2234    }
2235
2236    /// Begin bulk updates to Arrow store (Phase C)
2237    pub fn begin_bulk_update_arrow(
2238        &mut self,
2239    ) -> crate::engine::arrow_ingest::ArrowBulkUpdateBuilder<'_, R> {
2240        crate::engine::arrow_ingest::ArrowBulkUpdateBuilder::new(self)
2241    }
2242
2243    fn ensure_known_sheet_id(&self, sheet: &str) -> Result<SheetId, crate::engine::EditorError> {
2244        self.graph.sheet_id(sheet).ok_or(
2245            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
2246                name: sheet.to_string(),
2247                reason: "Unknown sheet".to_string(),
2248            },
2249        )
2250    }
2251
2252    fn normalize_row_1based(row_1based: u32) -> Result<u32, crate::engine::EditorError> {
2253        if row_1based == 0 {
2254            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
2255        }
2256        Ok(row_1based - 1)
2257    }
2258
2259    fn normalize_row_range_1based(
2260        start_row_1based: u32,
2261        end_row_1based: u32,
2262    ) -> Result<(u32, u32), crate::engine::EditorError> {
2263        if start_row_1based == 0 || end_row_1based == 0 {
2264            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
2265        }
2266        if start_row_1based > end_row_1based {
2267            return Err(crate::engine::EditorError::TransactionFailed {
2268                reason: "Row range start is greater than end".to_string(),
2269            });
2270        }
2271        Ok((start_row_1based - 1, end_row_1based - 1))
2272    }
2273
2274    fn invalidate_row_visibility_mask_cache(&self) {
2275        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
2276            cache.clear();
2277        }
2278    }
2279
2280    fn set_row_hidden_by_sheet_id(
2281        &mut self,
2282        sheet_id: SheetId,
2283        row0: u32,
2284        hidden: bool,
2285        source: RowVisibilitySource,
2286    ) -> bool {
2287        let changed = {
2288            let state = self.row_visibility.entry(sheet_id).or_default();
2289            state.set_row_hidden(row0, hidden, source)
2290        };
2291
2292        let remove_entry = self
2293            .row_visibility
2294            .get(&sheet_id)
2295            .map(|state| state.is_empty())
2296            .unwrap_or(false);
2297        if remove_entry {
2298            self.row_visibility.remove(&sheet_id);
2299        }
2300
2301        if changed {
2302            self.invalidate_row_visibility_mask_cache();
2303        }
2304
2305        changed
2306    }
2307
2308    fn set_rows_hidden_by_sheet_id(
2309        &mut self,
2310        sheet_id: SheetId,
2311        start_row0: u32,
2312        end_row0: u32,
2313        hidden: bool,
2314        source: RowVisibilitySource,
2315    ) -> bool {
2316        let changed = {
2317            let state = self.row_visibility.entry(sheet_id).or_default();
2318            state.set_rows_hidden(start_row0, end_row0, hidden, source)
2319        };
2320
2321        let remove_entry = self
2322            .row_visibility
2323            .get(&sheet_id)
2324            .map(|state| state.is_empty())
2325            .unwrap_or(false);
2326        if remove_entry {
2327            self.row_visibility.remove(&sheet_id);
2328        }
2329
2330        if changed {
2331            self.invalidate_row_visibility_mask_cache();
2332        }
2333
2334        changed
2335    }
2336
2337    fn shift_row_visibility_insert(&mut self, sheet_id: SheetId, before0: u32, count: u32) {
2338        if count == 0 {
2339            return;
2340        }
2341        let mut changed = false;
2342        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
2343            changed = state.insert_rows(before0, count);
2344            state.is_empty()
2345        } else {
2346            false
2347        };
2348        if remove_entry {
2349            self.row_visibility.remove(&sheet_id);
2350        }
2351        if changed {
2352            self.invalidate_row_visibility_mask_cache();
2353        }
2354    }
2355
2356    fn shift_row_visibility_delete(&mut self, sheet_id: SheetId, start0: u32, count: u32) {
2357        if count == 0 {
2358            return;
2359        }
2360        let mut changed = false;
2361        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
2362            changed = state.delete_rows(start0, count);
2363            state.is_empty()
2364        } else {
2365            false
2366        };
2367        if remove_entry {
2368            self.row_visibility.remove(&sheet_id);
2369        }
2370        if changed {
2371            self.invalidate_row_visibility_mask_cache();
2372        }
2373    }
2374
2375    fn apply_inverse_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
2376        if let crate::engine::ChangeEvent::SetRowVisibility {
2377            sheet_id,
2378            row0,
2379            source,
2380            old_hidden,
2381            ..
2382        } = event
2383        {
2384            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *old_hidden, *source);
2385        }
2386    }
2387
2388    fn apply_forward_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
2389        if let crate::engine::ChangeEvent::SetRowVisibility {
2390            sheet_id,
2391            row0,
2392            source,
2393            new_hidden,
2394            ..
2395        } = event
2396        {
2397            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *new_hidden, *source);
2398        }
2399    }
2400
2401    fn apply_inverse_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
2402        for event in events.iter().rev() {
2403            self.apply_inverse_row_visibility_event(event);
2404        }
2405    }
2406
2407    fn apply_forward_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
2408        for event in events {
2409            self.apply_forward_row_visibility_event(event);
2410        }
2411    }
2412
2413    pub fn set_row_hidden(
2414        &mut self,
2415        sheet: &str,
2416        row_1based: u32,
2417        hidden: bool,
2418        source: RowVisibilitySource,
2419    ) -> Result<(), crate::engine::EditorError> {
2420        let sheet_id = self.ensure_known_sheet_id(sheet)?;
2421        let row0 = Self::normalize_row_1based(row_1based)?;
2422        if self.set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source) {
2423            self.mark_data_edited();
2424        }
2425        Ok(())
2426    }
2427
2428    pub fn set_rows_hidden(
2429        &mut self,
2430        sheet: &str,
2431        start_row_1based: u32,
2432        end_row_1based: u32,
2433        hidden: bool,
2434        source: RowVisibilitySource,
2435    ) -> Result<(), crate::engine::EditorError> {
2436        let sheet_id = self.ensure_known_sheet_id(sheet)?;
2437        let (start_row0, end_row0) =
2438            Self::normalize_row_range_1based(start_row_1based, end_row_1based)?;
2439        if self.set_rows_hidden_by_sheet_id(sheet_id, start_row0, end_row0, hidden, source) {
2440            self.mark_data_edited();
2441        }
2442        Ok(())
2443    }
2444
2445    pub fn is_row_hidden(
2446        &self,
2447        sheet: &str,
2448        row_1based: u32,
2449        source: Option<RowVisibilitySource>,
2450    ) -> Option<bool> {
2451        let sheet_id = self.graph.sheet_id(sheet)?;
2452        let row0 = row_1based.checked_sub(1)?;
2453        Some(
2454            self.row_visibility
2455                .get(&sheet_id)
2456                .map(|state| state.is_row_hidden(row0, source))
2457                .unwrap_or(false),
2458        )
2459    }
2460
2461    pub fn row_visibility_version(&self, sheet: &str) -> Option<u64> {
2462        let sheet_id = self.graph.sheet_id(sheet)?;
2463        Some(
2464            self.row_visibility
2465                .get(&sheet_id)
2466                .map(|state| state.version())
2467                .unwrap_or(0),
2468        )
2469    }
2470
2471    fn build_row_visibility_mask_for_view(
2472        &self,
2473        view: &RangeView<'_>,
2474        mode: VisibilityMaskMode,
2475    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2476        let sheet_rows = view.sheet().nrows as usize;
2477        if sheet_rows == 0 || view.start_row() >= sheet_rows {
2478            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
2479        }
2480
2481        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
2482        let start_row0 = view.start_row() as u32;
2483        let end_row0 = view.end_row().min(sheet_rows.saturating_sub(1)) as u32;
2484        let version = self
2485            .row_visibility
2486            .get(&sheet_id)
2487            .map(|state| state.version())
2488            .unwrap_or(0);
2489        let key = VisibilityMaskCacheKey {
2490            sheet_id,
2491            start_row0,
2492            end_row0,
2493            mode,
2494            version,
2495        };
2496
2497        if let Ok(cache) = self.row_visibility_mask_cache.read()
2498            && let Some(mask) = cache.get(&key)
2499        {
2500            #[cfg(test)]
2501            visibility_mask_test_hooks::inc_hit();
2502            return Some(mask.clone());
2503        }
2504
2505        #[cfg(test)]
2506        visibility_mask_test_hooks::inc_miss();
2507
2508        let state = self.row_visibility.get(&sheet_id);
2509        let mut out = Vec::with_capacity((end_row0 - start_row0 + 1) as usize);
2510        for row0 in start_row0..=end_row0 {
2511            let manual_hidden = state
2512                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Manual)))
2513                .unwrap_or(false);
2514            let filter_hidden = state
2515                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Filter)))
2516                .unwrap_or(false);
2517
2518            let include = match mode {
2519                VisibilityMaskMode::IncludeAll => true,
2520                VisibilityMaskMode::ExcludeManualHidden => !manual_hidden,
2521                VisibilityMaskMode::ExcludeFilterHidden => !filter_hidden,
2522                VisibilityMaskMode::ExcludeManualOrFilterHidden => {
2523                    !(manual_hidden || filter_hidden)
2524                }
2525            };
2526            out.push(include);
2527        }
2528
2529        let mask = std::sync::Arc::new(arrow_array::BooleanArray::from(out));
2530        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
2531            const MAX_CACHE_ENTRIES: usize = 4096;
2532            if cache.len() >= MAX_CACHE_ENTRIES {
2533                cache.clear();
2534                #[cfg(test)]
2535                visibility_mask_test_hooks::inc_eviction();
2536            }
2537            cache.insert(key, mask.clone());
2538        }
2539
2540        Some(mask)
2541    }
2542
2543    /// Insert rows (1-based) and mirror into Arrow store when enabled
2544    pub fn insert_rows(
2545        &mut self,
2546        sheet: &str,
2547        before: u32,
2548        count: u32,
2549    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
2550    {
2551        use crate::engine::graph::editor::vertex_editor::VertexEditor;
2552        let sheet_id = self.ensure_known_sheet_id(sheet)?;
2553        let before0 = before.saturating_sub(1);
2554        let summary = {
2555            let mut editor = VertexEditor::new(&mut self.graph);
2556            editor.insert_rows(sheet_id, before0, count)?
2557        };
2558        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
2559            let before0 = before0 as usize;
2560            asheet.insert_rows(before0, count as usize);
2561        }
2562        self.shift_row_visibility_insert(sheet_id, before0, count);
2563        self.mark_topology_edited();
2564        Ok(summary)
2565    }
2566
2567    /// Delete rows (1-based) and mirror into Arrow store when enabled
2568    pub fn delete_rows(
2569        &mut self,
2570        sheet: &str,
2571        start: u32,
2572        count: u32,
2573    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
2574    {
2575        use crate::engine::graph::editor::vertex_editor::VertexEditor;
2576        let sheet_id = self.ensure_known_sheet_id(sheet)?;
2577        let start0 = start.saturating_sub(1);
2578        let summary = {
2579            let mut editor = VertexEditor::new(&mut self.graph);
2580            editor.delete_rows(sheet_id, start0, count)?
2581        };
2582        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
2583            let start0 = start0 as usize;
2584            asheet.delete_rows(start0, count as usize);
2585        }
2586        self.shift_row_visibility_delete(sheet_id, start0, count);
2587        self.mark_topology_edited();
2588        Ok(summary)
2589    }
2590
2591    /// Insert columns (1-based) and mirror into Arrow store when enabled
2592    pub fn insert_columns(
2593        &mut self,
2594        sheet: &str,
2595        before: u32,
2596        count: u32,
2597    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
2598    {
2599        use crate::engine::graph::editor::vertex_editor::VertexEditor;
2600        let sheet_id = self.graph.sheet_id(sheet).ok_or(
2601            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
2602                name: sheet.to_string(),
2603                reason: "Unknown sheet".to_string(),
2604            },
2605        )?;
2606        let before0 = before.saturating_sub(1);
2607        let summary = {
2608            let mut editor = VertexEditor::new(&mut self.graph);
2609            editor.insert_columns(sheet_id, before0, count)?
2610        };
2611        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
2612            let before0 = before0 as usize;
2613            asheet.insert_columns(before0, count as usize);
2614        }
2615        self.mark_topology_edited();
2616        Ok(summary)
2617    }
2618
2619    /// Delete columns (1-based) and mirror into Arrow store when enabled
2620    pub fn delete_columns(
2621        &mut self,
2622        sheet: &str,
2623        start: u32,
2624        count: u32,
2625    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
2626    {
2627        use crate::engine::graph::editor::vertex_editor::VertexEditor;
2628        let sheet_id = self.graph.sheet_id(sheet).ok_or(
2629            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
2630                name: sheet.to_string(),
2631                reason: "Unknown sheet".to_string(),
2632            },
2633        )?;
2634        let start0 = start.saturating_sub(1);
2635        let summary = {
2636            let mut editor = VertexEditor::new(&mut self.graph);
2637            editor.delete_columns(sheet_id, start0, count)?
2638        };
2639        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
2640            let start0 = start0 as usize;
2641            asheet.delete_columns(start0, count as usize);
2642        }
2643        self.mark_topology_edited();
2644        Ok(summary)
2645    }
2646    /// Arrow-backed used row bounds across a column span (1-based inclusive cols).
2647    fn arrow_used_row_bounds(
2648        &self,
2649        sheet: &str,
2650        start_col: u32,
2651        end_col: u32,
2652    ) -> Option<(u32, u32)> {
2653        let a = self.sheet_store().sheet(sheet)?;
2654        if a.columns.is_empty() {
2655            return None;
2656        }
2657        let sc0 = start_col.saturating_sub(1) as usize;
2658        let ec0 = end_col.saturating_sub(1) as usize;
2659        let col_hi = a.columns.len().saturating_sub(1);
2660        if sc0 > col_hi {
2661            return None;
2662        }
2663        let ec0 = ec0.min(col_hi);
2664        // Pass-scoped cache with snapshot guard
2665        let snap = self.data_snapshot_id();
2666        let mut min_r0: Option<usize> = None;
2667        for ci in sc0..=ec0 {
2668            let sheet_id = self.graph.sheet_id(sheet)?;
2669            if let Some((Some(mv), _)) = self.row_bounds_cache.read().ok().and_then(|g| {
2670                g.as_ref()
2671                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
2672            }) {
2673                let mv = mv as usize;
2674                min_r0 = Some(min_r0.map(|m| m.min(mv)).unwrap_or(mv));
2675                continue;
2676            }
2677            // Compute and store
2678            let (min_c, max_c) = Self::scan_column_used_bounds(a, ci);
2679            if let Ok(mut g) = self.row_bounds_cache.write() {
2680                g.get_or_insert_with(|| RowBoundsCache::new(snap))
2681                    .put_row_bounds(sheet_id, ci, snap, (min_c, max_c));
2682            }
2683            if let Some(m) = min_c {
2684                min_r0 = Some(min_r0.map(|mm| mm.min(m as usize)).unwrap_or(m as usize));
2685            }
2686        }
2687        min_r0?;
2688        let mut max_r0: Option<usize> = None;
2689        for ci in sc0..=ec0 {
2690            let sheet_id = self.graph.sheet_id(sheet)?;
2691            if let Some((_, Some(mv))) = self.row_bounds_cache.read().ok().and_then(|g| {
2692                g.as_ref()
2693                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
2694            }) {
2695                let mv = mv as usize;
2696                max_r0 = Some(max_r0.map(|m| m.max(mv)).unwrap_or(mv));
2697                continue;
2698            }
2699            let (_min_c, max_c) = Self::scan_column_used_bounds(a, ci);
2700            if let Ok(mut g) = self.row_bounds_cache.write() {
2701                g.get_or_insert_with(|| RowBoundsCache::new(snap))
2702                    .put_row_bounds(sheet_id, ci, snap, (_min_c, max_c));
2703            }
2704            if let Some(m) = max_c {
2705                max_r0 = Some(max_r0.map(|mm| mm.max(m as usize)).unwrap_or(m as usize));
2706            }
2707        }
2708        match (min_r0, max_r0) {
2709            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
2710            _ => None,
2711        }
2712    }
2713
2714    fn scan_column_used_bounds(
2715        a: &crate::arrow_store::ArrowSheet,
2716        ci: usize,
2717    ) -> (Option<u32>, Option<u32>) {
2718        let col = &a.columns[ci];
2719
2720        // Min: scan dense chunks first, then sparse chunks in ascending index order.
2721        let mut min_r0: Option<u32> = None;
2722        for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
2723            let tags = chunk.type_tag.values();
2724            for (off, &t) in tags.iter().enumerate() {
2725                let overlay_non_empty = chunk
2726                    .overlay
2727                    .get(off)
2728                    .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
2729                    .unwrap_or(false)
2730                    || chunk
2731                        .computed_overlay
2732                        .get(off)
2733                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
2734                        .unwrap_or(false);
2735                if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
2736                    let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
2737                        break;
2738                    };
2739                    let row0 = chunk_start + off;
2740                    min_r0 = Some(row0 as u32);
2741                    break;
2742                }
2743            }
2744            if min_r0.is_some() {
2745                break;
2746            }
2747        }
2748        if min_r0.is_none() && !col.sparse_chunks.is_empty() {
2749            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
2750            sparse_idxs.sort_unstable();
2751            for chunk_idx in sparse_idxs {
2752                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
2753                    continue;
2754                };
2755                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
2756                    continue;
2757                };
2758                let tags = chunk.type_tag.values();
2759                for (off, &t) in tags.iter().enumerate() {
2760                    let overlay_non_empty = chunk
2761                        .overlay
2762                        .get(off)
2763                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
2764                        .unwrap_or(false)
2765                        || chunk
2766                            .computed_overlay
2767                            .get(off)
2768                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
2769                            .unwrap_or(false);
2770                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
2771                        let row0 = chunk_start + off;
2772                        min_r0 = Some(row0 as u32);
2773                        break;
2774                    }
2775                }
2776                if min_r0.is_some() {
2777                    break;
2778                }
2779            }
2780        }
2781
2782        // Max: scan sparse chunks in descending index order, then dense chunks in reverse.
2783        let mut max_r0: Option<u32> = None;
2784        if !col.sparse_chunks.is_empty() {
2785            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
2786            sparse_idxs.sort_unstable_by(|a, b| b.cmp(a));
2787            for chunk_idx in sparse_idxs {
2788                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
2789                    continue;
2790                };
2791                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
2792                    continue;
2793                };
2794                let tags = chunk.type_tag.values();
2795                for (rev_idx, &t) in tags.iter().enumerate().rev() {
2796                    let overlay_non_empty = chunk
2797                        .overlay
2798                        .get(rev_idx)
2799                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
2800                        .unwrap_or(false)
2801                        || chunk
2802                            .computed_overlay
2803                            .get(rev_idx)
2804                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
2805                            .unwrap_or(false);
2806                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
2807                        let row0 = chunk_start + rev_idx;
2808                        max_r0 = Some(row0 as u32);
2809                        break;
2810                    }
2811                }
2812                if max_r0.is_some() {
2813                    break;
2814                }
2815            }
2816        }
2817        if max_r0.is_none() {
2818            for (chunk_idx, chunk) in col.chunks.iter().enumerate().rev() {
2819                let tags = chunk.type_tag.values();
2820                for (rev_idx, &t) in tags.iter().enumerate().rev() {
2821                    let overlay_non_empty = chunk
2822                        .overlay
2823                        .get(rev_idx)
2824                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
2825                        .unwrap_or(false)
2826                        || chunk
2827                            .computed_overlay
2828                            .get(rev_idx)
2829                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
2830                            .unwrap_or(false);
2831                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
2832                        let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
2833                            break;
2834                        };
2835                        let row0 = chunk_start + rev_idx;
2836                        max_r0 = Some(row0 as u32);
2837                        break;
2838                    }
2839                }
2840                if max_r0.is_some() {
2841                    break;
2842                }
2843            }
2844        }
2845
2846        (min_r0, max_r0)
2847    }
2848
2849    /// Arrow-backed used column bounds across a row span (1-based inclusive rows).
2850    fn arrow_used_col_bounds(
2851        &self,
2852        sheet: &str,
2853        start_row: u32,
2854        end_row: u32,
2855    ) -> Option<(u32, u32)> {
2856        let a = self.sheet_store().sheet(sheet)?;
2857        if a.columns.is_empty() {
2858            return None;
2859        }
2860        let sr0 = start_row.saturating_sub(1) as usize;
2861        let er0 = end_row.saturating_sub(1) as usize;
2862        if sr0 > er0 {
2863            return None;
2864        }
2865        // Map start/end rows into chunk ranges
2866        // We will scan each column for any non-empty within [sr0..=er0]
2867        let mut min_c0: Option<usize> = None;
2868        let mut max_c0: Option<usize> = None;
2869        // Precompute chunk bounds for row range
2870        for (ci, col) in a.columns.iter().enumerate() {
2871            let mut any_in_range = false;
2872
2873            let scan_chunk = |chunk_idx: usize, chunk: &crate::arrow_store::ColumnChunk| -> bool {
2874                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
2875                    return false;
2876                };
2877                let chunk_len = chunk.type_tag.len();
2878                if chunk_len == 0 {
2879                    return false;
2880                }
2881                let chunk_end = chunk_start + chunk_len.saturating_sub(1);
2882                // check intersection
2883                if sr0 > chunk_end || er0 < chunk_start {
2884                    return false;
2885                }
2886                let start_off = sr0.max(chunk_start) - chunk_start;
2887                let end_off = er0.min(chunk_end) - chunk_start;
2888                let tags = chunk.type_tag.values();
2889                for off in start_off..=end_off {
2890                    let overlay_non_empty = chunk
2891                        .overlay
2892                        .get(off)
2893                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
2894                        .unwrap_or(false)
2895                        || chunk
2896                            .computed_overlay
2897                            .get(off)
2898                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
2899                            .unwrap_or(false);
2900                    if overlay_non_empty || tags[off] != crate::arrow_store::TypeTag::Empty as u8 {
2901                        return true;
2902                    }
2903                }
2904                false
2905            };
2906
2907            for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
2908                if scan_chunk(chunk_idx, chunk) {
2909                    any_in_range = true;
2910                    break;
2911                }
2912            }
2913
2914            if !any_in_range && !col.sparse_chunks.is_empty() {
2915                for (&chunk_idx, chunk) in col.sparse_chunks.iter() {
2916                    if scan_chunk(chunk_idx, chunk) {
2917                        any_in_range = true;
2918                        break;
2919                    }
2920                }
2921            }
2922
2923            if any_in_range {
2924                min_c0 = Some(min_c0.map(|m| m.min(ci)).unwrap_or(ci));
2925                max_c0 = Some(max_c0.map(|m| m.max(ci)).unwrap_or(ci));
2926            }
2927        }
2928        match (min_c0, max_c0) {
2929            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
2930            _ => None,
2931        }
2932    }
2933
2934    fn formula_row_bounds_for_columns(
2935        &self,
2936        sheet: &str,
2937        start_col: u32,
2938        end_col: u32,
2939    ) -> Option<(u32, u32)> {
2940        let sheet_id = self.graph.sheet_id(sheet)?;
2941        let sc0 = start_col.saturating_sub(1);
2942        let ec0 = end_col.saturating_sub(1);
2943        let mut min_r0: Option<u32> = None;
2944        let mut max_r0: Option<u32> = None;
2945
2946        if let Some(index) = self.graph.sheet_index(sheet_id) {
2947            for vid in index.vertices_in_col_range(sc0, ec0) {
2948                if !matches!(
2949                    self.graph.get_vertex_kind(vid),
2950                    VertexKind::FormulaScalar | VertexKind::FormulaArray
2951                ) {
2952                    continue;
2953                }
2954                let row0 = self.graph.vertex_coord(vid).row();
2955                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
2956                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
2957            }
2958        } else {
2959            for vid in self.graph.vertices_in_sheet(sheet_id) {
2960                if !matches!(
2961                    self.graph.get_vertex_kind(vid),
2962                    VertexKind::FormulaScalar | VertexKind::FormulaArray
2963                ) {
2964                    continue;
2965                }
2966                let coord = self.graph.vertex_coord(vid);
2967                let col0 = coord.col();
2968                if col0 < sc0 || col0 > ec0 {
2969                    continue;
2970                }
2971                let row0 = coord.row();
2972                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
2973                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
2974            }
2975        }
2976
2977        match (min_r0, max_r0) {
2978            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
2979            _ => None,
2980        }
2981    }
2982
2983    fn formula_col_bounds_for_rows(
2984        &self,
2985        sheet: &str,
2986        start_row: u32,
2987        end_row: u32,
2988    ) -> Option<(u32, u32)> {
2989        let sheet_id = self.graph.sheet_id(sheet)?;
2990        let sr0 = start_row.saturating_sub(1);
2991        let er0 = end_row.saturating_sub(1);
2992        let mut min_c0: Option<u32> = None;
2993        let mut max_c0: Option<u32> = None;
2994
2995        if let Some(index) = self.graph.sheet_index(sheet_id) {
2996            for vid in index.vertices_in_row_range(sr0, er0) {
2997                if !matches!(
2998                    self.graph.get_vertex_kind(vid),
2999                    VertexKind::FormulaScalar | VertexKind::FormulaArray
3000                ) {
3001                    continue;
3002                }
3003                let col0 = self.graph.vertex_coord(vid).col();
3004                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
3005                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
3006            }
3007        } else {
3008            for vid in self.graph.vertices_in_sheet(sheet_id) {
3009                if !matches!(
3010                    self.graph.get_vertex_kind(vid),
3011                    VertexKind::FormulaScalar | VertexKind::FormulaArray
3012                ) {
3013                    continue;
3014                }
3015                let coord = self.graph.vertex_coord(vid);
3016                let row0 = coord.row();
3017                if row0 < sr0 || row0 > er0 {
3018                    continue;
3019                }
3020                let col0 = coord.col();
3021                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
3022                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
3023            }
3024        }
3025
3026        match (min_c0, max_c0) {
3027            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
3028            _ => None,
3029        }
3030    }
3031
3032    fn union_used_bounds(
3033        first: Option<(u32, u32)>,
3034        second: Option<(u32, u32)>,
3035    ) -> Option<(u32, u32)> {
3036        match (first, second) {
3037            (Some((a0, b0)), Some((a1, b1))) => Some((a0.min(a1), b0.max(b1))),
3038            (Some(bounds), None) | (None, Some(bounds)) => Some(bounds),
3039            (None, None) => None,
3040        }
3041    }
3042
3043    /// Mirror a single cell value into the Arrow overlay if enabled.
3044    /// Handles capacity growth, per-chunk overlay set, and heuristic compaction.
3045    fn mirror_value_to_overlay(&mut self, sheet: &str, row: u32, col: u32, value: &LiteralValue) {
3046        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
3047            return;
3048        }
3049        if self.arrow_sheets.sheet(sheet).is_none() {
3050            self.arrow_sheets
3051                .sheets
3052                .push(crate::arrow_store::ArrowSheet {
3053                    name: std::sync::Arc::<str>::from(sheet),
3054                    columns: Vec::new(),
3055                    nrows: 0,
3056                    chunk_starts: Vec::new(),
3057                    chunk_rows: 32 * 1024,
3058                });
3059        }
3060
3061        let row0 = row.saturating_sub(1) as usize;
3062        let col0 = col.saturating_sub(1) as usize;
3063
3064        let asheet = self
3065            .arrow_sheets
3066            .sheet_mut(sheet)
3067            .expect("ArrowSheet must exist");
3068
3069        let cur_cols = asheet.columns.len();
3070        if col0 >= cur_cols {
3071            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
3072        }
3073
3074        if row0 >= asheet.nrows as usize {
3075            if asheet.columns.is_empty() {
3076                asheet.insert_columns(0, 1);
3077            }
3078            asheet.ensure_row_capacity(row0 + 1);
3079        }
3080        if let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) {
3081            use crate::arrow_store::OverlayValue;
3082            let ov = match value {
3083                LiteralValue::Empty => OverlayValue::Empty,
3084                LiteralValue::Int(i) => OverlayValue::Number(*i as f64),
3085                LiteralValue::Number(n) => OverlayValue::Number(*n),
3086                LiteralValue::Boolean(b) => OverlayValue::Boolean(*b),
3087                LiteralValue::Text(s) => OverlayValue::Text(std::sync::Arc::from(s.clone())),
3088                LiteralValue::Error(e) => {
3089                    OverlayValue::Error(crate::arrow_store::map_error_code(e.kind))
3090                }
3091                LiteralValue::Date(d) => {
3092                    let dt = d.and_hms_opt(0, 0, 0).unwrap();
3093                    let serial = crate::builtins::datetime::datetime_to_serial_for(
3094                        self.config.date_system,
3095                        &dt,
3096                    );
3097                    OverlayValue::DateTime(serial)
3098                }
3099                LiteralValue::DateTime(dt) => {
3100                    let serial = crate::builtins::datetime::datetime_to_serial_for(
3101                        self.config.date_system,
3102                        dt,
3103                    );
3104                    OverlayValue::DateTime(serial)
3105                }
3106                LiteralValue::Time(t) => {
3107                    let serial = t.num_seconds_from_midnight() as f64 / 86_400.0;
3108                    OverlayValue::DateTime(serial)
3109                }
3110                LiteralValue::Duration(d) => {
3111                    let serial = d.num_seconds() as f64 / 86_400.0;
3112                    OverlayValue::Duration(serial)
3113                }
3114                LiteralValue::Pending => OverlayValue::Pending,
3115                LiteralValue::Array(_) => OverlayValue::Error(crate::arrow_store::map_error_code(
3116                    formualizer_common::ExcelErrorKind::Value,
3117                )),
3118            };
3119            if let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) {
3120                let _ = ch.overlay.set(in_off, ov);
3121                // A user edit must invalidate any computed (formula/spill) overlay entry at
3122                // this cell. Otherwise, if the delta overlay later compacts into the base lanes
3123                // (clearing `overlay`), a stale `computed_overlay=Empty` could incorrectly mask
3124                // the edited base value under the read cascade.
3125                let _ = ch.computed_overlay.remove(in_off);
3126            } else {
3127                return;
3128            }
3129            // Heuristic compaction: > len/50 or > 1024
3130            let abs_threshold = 1024usize;
3131            let frac_den = 50usize;
3132            let freed = asheet.maybe_compact_chunk(col0, ch_idx, abs_threshold, frac_den);
3133            if freed > 0 {
3134                self.overlay_compactions = self.overlay_compactions.saturating_add(1);
3135            }
3136        }
3137    }
3138
3139    /// Remove a delta-overlay entry for a single cell (if present).
3140    ///
3141    /// This is used when transitioning a cell to a formula so that any previous user-edit overlay
3142    /// does not continue to mask computed overlay outputs.
3143    fn clear_delta_overlay_cell(&mut self, sheet: &str, row: u32, col: u32) {
3144        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
3145            return;
3146        }
3147        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
3148            return;
3149        };
3150        let row0 = row.saturating_sub(1) as usize;
3151        let col0 = col.saturating_sub(1) as usize;
3152        if row0 >= asheet.nrows as usize {
3153            return;
3154        }
3155        if col0 >= asheet.columns.len() {
3156            return;
3157        }
3158        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
3159            return;
3160        };
3161        if let Some(ch) = asheet.columns[col0].chunk_mut(ch_idx) {
3162            let _ = ch.overlay.remove(in_off);
3163        }
3164    }
3165
3166    #[inline]
3167    fn overlay_value_to_literal(&self, ov: &crate::arrow_store::OverlayValue) -> LiteralValue {
3168        use crate::arrow_store::OverlayValue;
3169        match ov {
3170            OverlayValue::Empty => LiteralValue::Empty,
3171            OverlayValue::Number(n) => LiteralValue::Number(*n),
3172            OverlayValue::DateTime(serial) => LiteralValue::from_serial_number(*serial),
3173            OverlayValue::Duration(serial) => {
3174                let nanos_f = *serial * 86_400.0 * 1_000_000_000.0;
3175                let nanos = nanos_f.round().clamp(i64::MIN as f64, i64::MAX as f64) as i64;
3176                LiteralValue::Duration(chrono::Duration::nanoseconds(nanos))
3177            }
3178            OverlayValue::Boolean(b) => LiteralValue::Boolean(*b),
3179            OverlayValue::Text(s) => LiteralValue::Text((**s).to_string()),
3180            OverlayValue::Error(code) => {
3181                let kind = crate::arrow_store::unmap_error_code(*code);
3182                LiteralValue::Error(formualizer_common::ExcelError::new(kind))
3183            }
3184            OverlayValue::Pending => LiteralValue::Pending,
3185        }
3186    }
3187
3188    #[inline]
3189    fn literal_to_overlay_value(&self, value: &LiteralValue) -> crate::arrow_store::OverlayValue {
3190        use crate::arrow_store::OverlayValue;
3191        match value {
3192            LiteralValue::Empty => OverlayValue::Empty,
3193            LiteralValue::Int(i) => OverlayValue::Number(*i as f64),
3194            LiteralValue::Number(n) => OverlayValue::Number(*n),
3195            LiteralValue::Boolean(b) => OverlayValue::Boolean(*b),
3196            LiteralValue::Text(s) => OverlayValue::Text(std::sync::Arc::from(s.clone())),
3197            LiteralValue::Error(e) => {
3198                OverlayValue::Error(crate::arrow_store::map_error_code(e.kind))
3199            }
3200            LiteralValue::Date(d) => {
3201                let dt = d.and_hms_opt(0, 0, 0).unwrap();
3202                let serial =
3203                    crate::builtins::datetime::datetime_to_serial_for(self.config.date_system, &dt);
3204                OverlayValue::DateTime(serial)
3205            }
3206            LiteralValue::DateTime(dt) => {
3207                let serial =
3208                    crate::builtins::datetime::datetime_to_serial_for(self.config.date_system, dt);
3209                OverlayValue::DateTime(serial)
3210            }
3211            LiteralValue::Time(t) => {
3212                let serial = t.num_seconds_from_midnight() as f64 / 86_400.0;
3213                OverlayValue::DateTime(serial)
3214            }
3215            LiteralValue::Duration(d) => {
3216                let serial = d.num_seconds() as f64 / 86_400.0;
3217                OverlayValue::Duration(serial)
3218            }
3219            LiteralValue::Pending => OverlayValue::Pending,
3220            LiteralValue::Array(_) => OverlayValue::Error(crate::arrow_store::map_error_code(
3221                formualizer_common::ExcelErrorKind::Value,
3222            )),
3223        }
3224    }
3225
3226    /// Read a single cell's delta overlay entry (if present), preserving the distinction between
3227    /// absent and explicit `Empty`.
3228    fn read_delta_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
3229        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
3230            return None;
3231        }
3232        let asheet = self.arrow_sheets.sheet(sheet)?;
3233        let row0 = row.saturating_sub(1) as usize;
3234        let col0 = col.saturating_sub(1) as usize;
3235        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
3236            return None;
3237        }
3238        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
3239        let ch = asheet.columns[col0].chunk(ch_idx)?;
3240        ch.overlay
3241            .get(in_off)
3242            .map(|ov| self.overlay_value_to_literal(ov))
3243    }
3244
3245    /// Read a single cell's computed overlay entry (if present), preserving the distinction
3246    /// between absent and explicit `Empty`.
3247    fn read_computed_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
3248        if !(self.config.arrow_storage_enabled
3249            && self.config.delta_overlay_enabled
3250            && self.config.write_formula_overlay_enabled)
3251        {
3252            return None;
3253        }
3254        let asheet = self.arrow_sheets.sheet(sheet)?;
3255        let row0 = row.saturating_sub(1) as usize;
3256        let col0 = col.saturating_sub(1) as usize;
3257        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
3258            return None;
3259        }
3260        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
3261        let ch = asheet.columns[col0].chunk(ch_idx)?;
3262        ch.computed_overlay
3263            .get(in_off)
3264            .map(|ov| self.overlay_value_to_literal(ov))
3265    }
3266
3267    fn set_delta_overlay_cell_raw(
3268        &mut self,
3269        sheet: &str,
3270        row: u32,
3271        col: u32,
3272        value: Option<LiteralValue>,
3273    ) {
3274        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
3275            return;
3276        }
3277
3278        self.ensure_arrow_sheet(sheet);
3279        let ov_opt = value.as_ref().map(|v| self.literal_to_overlay_value(v));
3280        let row0 = row.saturating_sub(1) as usize;
3281        let col0 = col.saturating_sub(1) as usize;
3282        let asheet = self
3283            .arrow_sheets
3284            .sheet_mut(sheet)
3285            .expect("ArrowSheet must exist");
3286
3287        let cur_cols = asheet.columns.len();
3288        if col0 >= cur_cols {
3289            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
3290        }
3291        if row0 >= asheet.nrows as usize {
3292            if asheet.columns.is_empty() {
3293                asheet.insert_columns(0, 1);
3294            }
3295            asheet.ensure_row_capacity(row0 + 1);
3296        }
3297
3298        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
3299            return;
3300        };
3301        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
3302            return;
3303        };
3304
3305        if let Some(ov) = ov_opt {
3306            let _ = ch.overlay.set(in_off, ov);
3307        } else {
3308            let _ = ch.overlay.remove(in_off);
3309        }
3310    }
3311
3312    fn set_computed_overlay_cell_raw(
3313        &mut self,
3314        sheet: &str,
3315        row: u32,
3316        col: u32,
3317        value: Option<LiteralValue>,
3318    ) {
3319        if !(self.config.arrow_storage_enabled
3320            && self.config.delta_overlay_enabled
3321            && self.config.write_formula_overlay_enabled)
3322        {
3323            return;
3324        }
3325
3326        self.ensure_arrow_sheet(sheet);
3327        let ov_opt = value.as_ref().map(|v| self.literal_to_overlay_value(v));
3328        let row0 = row.saturating_sub(1) as usize;
3329        let col0 = col.saturating_sub(1) as usize;
3330        let asheet = self
3331            .arrow_sheets
3332            .sheet_mut(sheet)
3333            .expect("ArrowSheet must exist");
3334
3335        let cur_cols = asheet.columns.len();
3336        if col0 >= cur_cols {
3337            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
3338        }
3339        if row0 >= asheet.nrows as usize {
3340            if asheet.columns.is_empty() {
3341                asheet.insert_columns(0, 1);
3342            }
3343            asheet.ensure_row_capacity(row0 + 1);
3344        }
3345
3346        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
3347            return;
3348        };
3349        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
3350            return;
3351        };
3352
3353        let delta = if let Some(ov) = ov_opt {
3354            ch.computed_overlay.set(in_off, ov)
3355        } else {
3356            ch.computed_overlay.remove(in_off)
3357        };
3358        self.adjust_computed_overlay_bytes(delta);
3359    }
3360
3361    fn apply_arrow_undo_batch(&mut self, batch: &crate::engine::ArrowUndoBatch, undo: bool) {
3362        use crate::engine::ArrowOp;
3363
3364        let iter: Box<dyn Iterator<Item = &ArrowOp>> = if undo {
3365            Box::new(batch.ops.iter().rev())
3366        } else {
3367            Box::new(batch.ops.iter())
3368        };
3369
3370        for op in iter {
3371            match op {
3372                ArrowOp::SetDeltaCell {
3373                    sheet_id,
3374                    row0,
3375                    col0,
3376                    old,
3377                    new,
3378                } => {
3379                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
3380                    let v = if undo { old.clone() } else { new.clone() };
3381                    self.set_delta_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
3382                }
3383                ArrowOp::SetComputedCell {
3384                    sheet_id,
3385                    row0,
3386                    col0,
3387                    old,
3388                    new,
3389                } => {
3390                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
3391                    let v = if undo { old.clone() } else { new.clone() };
3392                    self.set_computed_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
3393                }
3394                ArrowOp::RestoreComputedRect {
3395                    sheet_id,
3396                    sr0,
3397                    sc0,
3398                    er0,
3399                    ec0,
3400                    old,
3401                    new,
3402                } => {
3403                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
3404                    let vals = if undo { old } else { new };
3405                    let height = (*er0).saturating_sub(*sr0) as usize + 1;
3406                    let width = (*ec0).saturating_sub(*sc0) as usize + 1;
3407                    for r in 0..height {
3408                        for c in 0..width {
3409                            let v = vals
3410                                .get(r)
3411                                .and_then(|row| row.get(c))
3412                                .cloned()
3413                                .unwrap_or(LiteralValue::Empty);
3414                            self.set_computed_overlay_cell_raw(
3415                                &sheet,
3416                                *sr0 + 1 + r as u32,
3417                                *sc0 + 1 + c as u32,
3418                                Some(v),
3419                            );
3420                        }
3421                    }
3422                }
3423                ArrowOp::InsertRows {
3424                    sheet_id,
3425                    before0,
3426                    count,
3427                } => {
3428                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
3429                    self.ensure_arrow_sheet(&sheet);
3430                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
3431                        if undo {
3432                            asheet.delete_rows(*before0 as usize, *count as usize);
3433                        } else {
3434                            asheet.insert_rows(*before0 as usize, *count as usize);
3435                        }
3436                    }
3437                }
3438                ArrowOp::InsertCols {
3439                    sheet_id,
3440                    before0,
3441                    count,
3442                } => {
3443                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
3444                    self.ensure_arrow_sheet(&sheet);
3445                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
3446                        if undo {
3447                            asheet.delete_columns(*before0 as usize, *count as usize);
3448                        } else {
3449                            asheet.insert_columns(*before0 as usize, *count as usize);
3450                        }
3451                    }
3452                }
3453            }
3454        }
3455    }
3456
3457    fn record_spill_ops_into_arrow_undo(
3458        &mut self,
3459        undo: &mut crate::engine::ArrowUndoBatch,
3460        events: &[crate::engine::ChangeEvent],
3461    ) {
3462        use crate::engine::ChangeEvent;
3463        use formualizer_common::LiteralValue;
3464
3465        #[allow(clippy::type_complexity)]
3466        let rect_from_snapshot =
3467            |snap: &crate::engine::graph::editor::change_log::SpillSnapshot|
3468             -> Option<(SheetId, u32, u32, u32, u32, Vec<Vec<LiteralValue>>)> {
3469                if snap.target_cells.is_empty() {
3470                    return None;
3471                }
3472                let sheet_id = snap.target_cells[0].sheet_id;
3473                let sr0 = snap.target_cells[0].coord.row();
3474                let sc0 = snap.target_cells[0].coord.col();
3475                if snap.values.is_empty() || snap.values[0].is_empty() {
3476                    return None;
3477                }
3478                let h = snap.values.len() as u32;
3479                let w = snap.values[0].len() as u32;
3480                let er0 = sr0.saturating_add(h.saturating_sub(1));
3481                let ec0 = sc0.saturating_add(w.saturating_sub(1));
3482                Some((sheet_id, sr0, sc0, er0, ec0, snap.values.clone()))
3483            };
3484
3485        for ev in events {
3486            match ev {
3487                ChangeEvent::SpillCommitted { old, new, .. } => {
3488                    if let Some((sid, sr0, sc0, er0, ec0, new_vals)) = rect_from_snapshot(new) {
3489                        let old_vals = if let Some(old_snap) = old {
3490                            rect_from_snapshot(old_snap)
3491                                .map(|(_, _, _, _, _, v)| v)
3492                                .unwrap_or_else(|| {
3493                                    vec![
3494                                        vec![LiteralValue::Empty; new_vals[0].len()];
3495                                        new_vals.len()
3496                                    ]
3497                                })
3498                        } else {
3499                            vec![vec![LiteralValue::Empty; new_vals[0].len()]; new_vals.len()]
3500                        };
3501                        undo.record_restore_computed_rect(
3502                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
3503                        );
3504                    }
3505                }
3506                ChangeEvent::SpillCleared { old, .. } => {
3507                    if let Some((sid, sr0, sc0, er0, ec0, old_vals)) = rect_from_snapshot(old) {
3508                        let new_vals =
3509                            vec![vec![LiteralValue::Empty; old_vals[0].len()]; old_vals.len()];
3510                        undo.record_restore_computed_rect(
3511                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
3512                        );
3513                    }
3514                }
3515                _ => {}
3516            }
3517        }
3518    }
3519
3520    /// Mirror a value into the computed overlay (formula/spill outputs).
3521    ///
3522    /// This path is subject to `EvalConfig.max_overlay_memory_bytes`.
3523    /// If the cap is exceeded, we deterministically stop mirroring additional computed overlay
3524    /// values and force RangeView resolution to materialize from graph values for correctness.
3525    fn mirror_value_to_computed_overlay(
3526        &mut self,
3527        sheet: &str,
3528        row: u32,
3529        col: u32,
3530        value: &LiteralValue,
3531    ) {
3532        if !(self.config.arrow_storage_enabled
3533            && self.config.delta_overlay_enabled
3534            && self.config.write_formula_overlay_enabled)
3535        {
3536            return;
3537        }
3538        if self.computed_overlay_mirroring_disabled {
3539            return;
3540        }
3541
3542        if self.arrow_sheets.sheet(sheet).is_none() {
3543            self.arrow_sheets
3544                .sheets
3545                .push(crate::arrow_store::ArrowSheet {
3546                    name: std::sync::Arc::<str>::from(sheet),
3547                    columns: Vec::new(),
3548                    nrows: 0,
3549                    chunk_starts: Vec::new(),
3550                    chunk_rows: 32 * 1024,
3551                });
3552        }
3553
3554        let row0 = row.saturating_sub(1) as usize;
3555        let col0 = col.saturating_sub(1) as usize;
3556
3557        let asheet = self
3558            .arrow_sheets
3559            .sheet_mut(sheet)
3560            .expect("ArrowSheet must exist");
3561
3562        let cur_cols = asheet.columns.len();
3563        if col0 >= cur_cols {
3564            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
3565        }
3566
3567        if row0 >= asheet.nrows as usize {
3568            if asheet.columns.is_empty() {
3569                asheet.insert_columns(0, 1);
3570            }
3571            asheet.ensure_row_capacity(row0 + 1);
3572        }
3573
3574        if let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) {
3575            use crate::arrow_store::OverlayValue;
3576            let ov = match value {
3577                LiteralValue::Empty => OverlayValue::Empty,
3578                LiteralValue::Int(i) => OverlayValue::Number(*i as f64),
3579                LiteralValue::Number(n) => OverlayValue::Number(*n),
3580                LiteralValue::Boolean(b) => OverlayValue::Boolean(*b),
3581                LiteralValue::Text(s) => OverlayValue::Text(std::sync::Arc::from(s.clone())),
3582                LiteralValue::Error(e) => {
3583                    OverlayValue::Error(crate::arrow_store::map_error_code(e.kind))
3584                }
3585                LiteralValue::Date(d) => {
3586                    let dt = d.and_hms_opt(0, 0, 0).unwrap();
3587                    let serial = crate::builtins::datetime::datetime_to_serial_for(
3588                        self.config.date_system,
3589                        &dt,
3590                    );
3591                    OverlayValue::DateTime(serial)
3592                }
3593                LiteralValue::DateTime(dt) => {
3594                    let serial = crate::builtins::datetime::datetime_to_serial_for(
3595                        self.config.date_system,
3596                        dt,
3597                    );
3598                    OverlayValue::DateTime(serial)
3599                }
3600                LiteralValue::Time(t) => {
3601                    let serial = t.num_seconds_from_midnight() as f64 / 86_400.0;
3602                    OverlayValue::DateTime(serial)
3603                }
3604                LiteralValue::Duration(d) => {
3605                    let serial = d.num_seconds() as f64 / 86_400.0;
3606                    OverlayValue::Duration(serial)
3607                }
3608                LiteralValue::Pending => OverlayValue::Pending,
3609                LiteralValue::Array(_) => OverlayValue::Error(crate::arrow_store::map_error_code(
3610                    formualizer_common::ExcelErrorKind::Value,
3611                )),
3612            };
3613
3614            let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
3615                return;
3616            };
3617
3618            let delta = ch.computed_overlay.set(in_off, ov);
3619            self.adjust_computed_overlay_bytes(delta);
3620
3621            if let Some(cap) = self.config.max_overlay_memory_bytes
3622                && self.computed_overlay_bytes_estimate > cap
3623            {
3624                self.disable_computed_overlay_mirroring_due_to_budget(cap);
3625            }
3626        }
3627    }
3628
3629    #[inline]
3630    fn adjust_computed_overlay_bytes(&mut self, delta: isize) {
3631        if delta >= 0 {
3632            self.computed_overlay_bytes_estimate = self
3633                .computed_overlay_bytes_estimate
3634                .saturating_add(delta as usize);
3635        } else {
3636            self.computed_overlay_bytes_estimate = self
3637                .computed_overlay_bytes_estimate
3638                .saturating_sub((-delta) as usize);
3639        }
3640    }
3641
3642    fn clear_all_computed_overlays(&mut self) {
3643        let mut freed_total = 0usize;
3644        for sh in self.arrow_sheets.sheets.iter_mut() {
3645            for col in sh.columns.iter_mut() {
3646                for ch in col.chunks.iter_mut() {
3647                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
3648                }
3649                for ch in col.sparse_chunks.values_mut() {
3650                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
3651                }
3652            }
3653        }
3654        self.computed_overlay_bytes_estimate = self
3655            .computed_overlay_bytes_estimate
3656            .saturating_sub(freed_total);
3657    }
3658
3659    fn disable_computed_overlay_mirroring_due_to_budget(&mut self, _cap: usize) {
3660        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
3661        // Handle budget pressure by compacting computed overlays into base lanes.
3662        self.compact_all_computed_overlays();
3663    }
3664
3665    /// Fold all computed overlay entries across all sheets into their base arrays.
3666    /// This preserves data while freeing overlay memory, allowing mirroring to continue.
3667    fn compact_all_computed_overlays(&mut self) {
3668        let mut freed_total = 0usize;
3669        for sheet in self.arrow_sheets.sheets.iter_mut() {
3670            for col_idx in 0..sheet.columns.len() {
3671                // Dense chunks
3672                let num_dense = sheet.columns[col_idx].chunks.len();
3673                for ch_idx in 0..num_dense {
3674                    freed_total += sheet.compact_computed_overlay_chunk(col_idx, ch_idx);
3675                }
3676                // Sparse chunks
3677                let sparse_keys: Vec<usize> = sheet.columns[col_idx]
3678                    .sparse_chunks
3679                    .keys()
3680                    .copied()
3681                    .collect();
3682                for ch_idx in sparse_keys {
3683                    freed_total += sheet.compact_computed_overlay_sparse_chunk(col_idx, ch_idx);
3684                }
3685            }
3686        }
3687        self.computed_overlay_bytes_estimate = self
3688            .computed_overlay_bytes_estimate
3689            .saturating_sub(freed_total);
3690        self.overlay_compactions = self.overlay_compactions.saturating_add(1);
3691    }
3692
3693    fn mirror_vertex_value_to_overlay(&mut self, vertex_id: VertexId, value: &LiteralValue) {
3694        if !(self.config.arrow_storage_enabled
3695            && self.config.delta_overlay_enabled
3696            && self.config.write_formula_overlay_enabled)
3697        {
3698            return;
3699        }
3700        if !matches!(
3701            self.graph.get_vertex_kind(vertex_id),
3702            VertexKind::FormulaScalar | VertexKind::FormulaArray
3703        ) {
3704            return;
3705        }
3706        let Some(cell) = self.graph.get_cell_ref(vertex_id) else {
3707            return;
3708        };
3709        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
3710        self.mirror_value_to_computed_overlay(
3711            &sheet_name,
3712            cell.coord.row() + 1,
3713            cell.coord.col() + 1,
3714            value,
3715        );
3716    }
3717
3718    /// Estimated memory usage for computed overlays (formula/spill mirroring).
3719    pub fn overlay_memory_usage(&self) -> usize {
3720        self.computed_overlay_bytes_estimate
3721    }
3722
3723    fn resolve_sheet_locator_for_write(
3724        &mut self,
3725        loc: formualizer_common::SheetLocator<'_>,
3726        current_sheet: &str,
3727    ) -> Result<SheetId, ExcelError> {
3728        Ok(match loc {
3729            formualizer_common::SheetLocator::Id(id) => id,
3730            formualizer_common::SheetLocator::Name(name) => self.graph.sheet_id_mut(name.as_ref()),
3731            formualizer_common::SheetLocator::Current => self.graph.sheet_id_mut(current_sheet),
3732        })
3733    }
3734
3735    fn resolve_sheet_locator_for_read(
3736        &self,
3737        loc: formualizer_common::SheetLocator<'_>,
3738        current_sheet: &str,
3739    ) -> Result<SheetId, ExcelError> {
3740        match loc {
3741            formualizer_common::SheetLocator::Id(id) => Ok(id),
3742            formualizer_common::SheetLocator::Name(name) => self
3743                .graph
3744                .sheet_id(name.as_ref())
3745                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
3746            formualizer_common::SheetLocator::Current => self
3747                .graph
3748                .sheet_id(current_sheet)
3749                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
3750        }
3751    }
3752
3753    /// Set a cell value
3754    pub fn set_cell_value(
3755        &mut self,
3756        sheet: &str,
3757        row: u32,
3758        col: u32,
3759        value: LiteralValue,
3760    ) -> Result<(), ExcelError> {
3761        self.graph.set_cell_value(sheet, row, col, value.clone())?;
3762        // Mirror into Arrow overlay when enabled
3763        self.mirror_value_to_overlay(sheet, row, col, &value);
3764        // Advance snapshot to reflect external mutation
3765        self.snapshot_id
3766            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3767        self.has_edited = true;
3768        Ok(())
3769    }
3770
3771    pub fn set_cell_value_ref(
3772        &mut self,
3773        cell: formualizer_common::SheetCellRef<'_>,
3774        current_sheet: &str,
3775        value: LiteralValue,
3776    ) -> Result<(), ExcelError> {
3777        let owned = cell.into_owned();
3778        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
3779        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
3780        self.set_cell_value(
3781            &sheet_name,
3782            owned.coord.row() + 1,
3783            owned.coord.col() + 1,
3784            value,
3785        )
3786    }
3787
3788    pub fn set_cell_formula_ref(
3789        &mut self,
3790        cell: formualizer_common::SheetCellRef<'_>,
3791        current_sheet: &str,
3792        ast: ASTNode,
3793    ) -> Result<(), ExcelError> {
3794        let owned = cell.into_owned();
3795        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
3796        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
3797        self.set_cell_formula(
3798            &sheet_name,
3799            owned.coord.row() + 1,
3800            owned.coord.col() + 1,
3801            ast,
3802        )
3803    }
3804
3805    pub fn get_cell_value_ref(
3806        &self,
3807        cell: formualizer_common::SheetCellRef<'_>,
3808        current_sheet: &str,
3809    ) -> Result<Option<LiteralValue>, ExcelError> {
3810        let owned = cell.into_owned();
3811        let sheet_id = self.resolve_sheet_locator_for_read(owned.sheet, current_sheet)?;
3812        let sheet_name = self.graph.sheet_name(sheet_id);
3813        Ok(self.get_cell_value(sheet_name, owned.coord.row() + 1, owned.coord.col() + 1))
3814    }
3815
3816    pub fn resolve_range_view_sheet_ref<'c>(
3817        &'c self,
3818        r: &formualizer_common::SheetRef<'_>,
3819        current_sheet: &str,
3820    ) -> Result<RangeView<'c>, ExcelError> {
3821        use formualizer_common::SheetLocator;
3822
3823        let sheet_to_opt_name = |loc: SheetLocator<'_>| -> Result<Option<String>, ExcelError> {
3824            match loc {
3825                SheetLocator::Current => Ok(None),
3826                SheetLocator::Name(name) => Ok(Some(name.as_ref().to_string())),
3827                SheetLocator::Id(id) => Ok(Some(self.graph.sheet_name(id).to_string())),
3828            }
3829        };
3830
3831        let rt = match r {
3832            formualizer_common::SheetRef::Cell(cell) => ReferenceType::Cell {
3833                sheet: sheet_to_opt_name(cell.sheet.clone())?,
3834                row: cell.coord.row() + 1,
3835                col: cell.coord.col() + 1,
3836                row_abs: cell.coord.row_abs(),
3837                col_abs: cell.coord.col_abs(),
3838            },
3839            formualizer_common::SheetRef::Range(range) => ReferenceType::Range {
3840                sheet: sheet_to_opt_name(range.sheet.clone())?,
3841                start_row: range.start_row.map(|b| b.index + 1),
3842                start_col: range.start_col.map(|b| b.index + 1),
3843                end_row: range.end_row.map(|b| b.index + 1),
3844                end_col: range.end_col.map(|b| b.index + 1),
3845                start_row_abs: range.start_row.map(|b| b.abs).unwrap_or(false),
3846                start_col_abs: range.start_col.map(|b| b.abs).unwrap_or(false),
3847                end_row_abs: range.end_row.map(|b| b.abs).unwrap_or(false),
3848                end_col_abs: range.end_col.map(|b| b.abs).unwrap_or(false),
3849            },
3850        };
3851
3852        crate::traits::EvaluationContext::resolve_range_view(self, &rt, current_sheet)
3853    }
3854
3855    /// Set a cell formula
3856    pub fn set_cell_formula(
3857        &mut self,
3858        sheet: &str,
3859        row: u32,
3860        col: u32,
3861        ast: ASTNode,
3862    ) -> Result<(), ExcelError> {
3863        let volatile = self.is_ast_volatile_with_provider(&ast);
3864        self.graph
3865            .set_cell_formula_with_volatility(sheet, row, col, ast, volatile)?;
3866
3867        // If the cell previously held a user value in the delta overlay, it must not continue
3868        // to mask the formula result under Arrow-canonical reads (overlay precedence is
3869        // delta -> computed -> base). Remove the overlay entry instead of writing `Empty`,
3870        // because an explicit `Empty` overlay would still take precedence over computed values.
3871        self.clear_delta_overlay_cell(sheet, row, col);
3872
3873        // Advance snapshot to reflect external mutation
3874        self.mark_topology_edited();
3875        Ok(())
3876    }
3877
3878    /// Bulk set many formulas on a sheet. Skips per-cell snapshot bumping and minimizes edge rebuilds.
3879    pub fn bulk_set_formulas<I>(&mut self, sheet: &str, items: I) -> Result<usize, ExcelError>
3880    where
3881        I: IntoIterator<Item = (u32, u32, ASTNode)>,
3882    {
3883        let collected: Vec<(u32, u32, ASTNode)> = items.into_iter().collect();
3884        let vol_flags: Vec<bool> = collected
3885            .iter()
3886            .map(|(_, _, ast)| self.is_ast_volatile_with_provider(ast))
3887            .collect();
3888        let n = self
3889            .graph
3890            .bulk_set_formulas_with_volatility(sheet, collected, vol_flags)?;
3891        // Single topology bump after batch
3892        if n > 0 {
3893            self.mark_topology_edited();
3894        }
3895        Ok(n)
3896    }
3897
3898    #[inline]
3899    fn normalize_public_cell_read(v: LiteralValue) -> Option<LiteralValue> {
3900        match v {
3901            LiteralValue::Empty => None,
3902            LiteralValue::Int(i) => Some(LiteralValue::Number(i as f64)),
3903            other => Some(other),
3904        }
3905    }
3906
3907    /// Get a cell value
3908    pub fn get_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
3909        self.read_cell_value(sheet, row, col)
3910            .and_then(Self::normalize_public_cell_read)
3911    }
3912
3913    /// Unified internal read API for a single cell value (Arrow-truth).
3914    pub(crate) fn read_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
3915        let asheet = self.sheet_store().sheet(sheet)?;
3916        let r0 = row.saturating_sub(1) as usize;
3917        let c0 = col.saturating_sub(1) as usize;
3918        let v = asheet.get_cell_value(r0, c0);
3919        if matches!(v, LiteralValue::Empty) {
3920            None
3921        } else {
3922            Some(v)
3923        }
3924    }
3925
3926    /// Unified internal read API for a range of cell values (Arrow-truth).
3927    pub(crate) fn read_range_values(
3928        &self,
3929        sheet: &str,
3930        sr: u32,
3931        sc: u32,
3932        er: u32,
3933        ec: u32,
3934    ) -> RangeView<'_> {
3935        let Some(asheet) = self.sheet_store().sheet(sheet) else {
3936            return RangeView::from_owned_rows(Vec::new(), self.config.date_system);
3937        };
3938        if er < sr || ec < sc {
3939            return asheet.range_view(1, 1, 0, 0);
3940        }
3941        let sr0 = sr.saturating_sub(1) as usize;
3942        let sc0 = sc.saturating_sub(1) as usize;
3943        let er0 = er.saturating_sub(1) as usize;
3944        let ec0 = ec.saturating_sub(1) as usize;
3945        asheet.range_view(sr0, sc0, er0, ec0)
3946    }
3947
3948    /// Get formula AST (if any) and current stored value for a cell
3949    pub fn get_cell(
3950        &self,
3951        sheet: &str,
3952        row: u32,
3953        col: u32,
3954    ) -> Option<(Option<formualizer_parse::ASTNode>, Option<LiteralValue>)> {
3955        let v = self.get_cell_value(sheet, row, col);
3956        let sheet_id = self.graph.sheet_id(sheet)?;
3957        let coord = Coord::from_excel(row, col, true, true);
3958        let cell = CellRef::new(sheet_id, coord);
3959        let vid = self.graph.get_vertex_for_cell(&cell)?;
3960        let ast = self.graph.get_formula_id(vid).and_then(|ast_id| {
3961            self.graph
3962                .data_store()
3963                .retrieve_ast(ast_id, self.graph.sheet_reg())
3964        });
3965        Some((ast, v))
3966    }
3967
3968    /// Begin batch operations - defer CSR rebuilds for better performance
3969    pub fn begin_batch(&mut self) {
3970        self.graph.begin_batch();
3971    }
3972
3973    /// End batch operations and trigger CSR rebuild
3974    pub fn end_batch(&mut self) {
3975        self.graph.end_batch();
3976    }
3977
3978    /// Evaluate a single vertex.
3979    /// This is the core of the sequential evaluation logic for Milestone 3.1.
3980    #[inline]
3981    fn record_cell_if_changed(
3982        delta: &mut DeltaCollector,
3983        cell: &CellRef,
3984        old: &LiteralValue,
3985        new: &LiteralValue,
3986    ) {
3987        if old != new {
3988            delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
3989        }
3990    }
3991
3992    pub fn evaluate_vertex(&mut self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
3993        self.evaluate_vertex_impl(vertex_id, None)
3994    }
3995
3996    fn evaluate_vertex_impl(
3997        &mut self,
3998        vertex_id: VertexId,
3999        delta: Option<&mut DeltaCollector>,
4000    ) -> Result<LiteralValue, ExcelError> {
4001        let mut delta = delta;
4002        // Check if vertex exists
4003        if !self.graph.vertex_exists(vertex_id) {
4004            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
4005                .with_message(format!("Vertex not found: {vertex_id:?}")));
4006        }
4007
4008        // Get vertex kind and check if it needs evaluation
4009        let kind = self.graph.get_vertex_kind(vertex_id);
4010        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
4011
4012        let ast_id = match kind {
4013            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
4014                if let Some(ast_id) = self.graph.get_formula_id(vertex_id) {
4015                    ast_id
4016                } else {
4017                    return Ok(LiteralValue::Number(0.0));
4018                }
4019            }
4020            VertexKind::Empty | VertexKind::Cell => {
4021                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
4022                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
4023                    let row = cell_ref.coord.row() + 1;
4024                    let col = cell_ref.coord.col() + 1;
4025                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
4026                        return Ok(v);
4027                    }
4028                }
4029                return Ok(LiteralValue::Number(0.0));
4030            }
4031            VertexKind::NamedScalar => {
4032                let value = self.evaluate_named_scalar(vertex_id, sheet_id)?;
4033                return Ok(value);
4034            }
4035            VertexKind::NamedArray => {
4036                let value = self.evaluate_named_array(vertex_id, sheet_id)?;
4037                return Ok(value);
4038            }
4039            VertexKind::InfiniteRange
4040            | VertexKind::Range
4041            | VertexKind::External
4042            | VertexKind::Table => {
4043                // Not directly evaluatable here.
4044                return Ok(LiteralValue::Number(0.0));
4045            }
4046        };
4047
4048        // The interpreter uses a reference to the engine as the context.
4049        let sheet_name = self.graph.sheet_name(sheet_id);
4050        let cell_ref = self
4051            .graph
4052            .get_cell_ref(vertex_id)
4053            .expect("cell ref for vertex");
4054        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
4055
4056        let result =
4057            interpreter.evaluate_arena_ast(ast_id, self.graph.data_store(), self.graph.sheet_reg());
4058
4059        // If array result, perform spill from the anchor cell
4060        match result {
4061            Ok(cv) => {
4062                let result_literal = cv.into_literal();
4063                match result_literal {
4064                    LiteralValue::Array(rows) => {
4065                        // Update kind to FormulaArray for tracking
4066                        self.graph
4067                            .set_kind(vertex_id, crate::engine::vertex::VertexKind::FormulaArray);
4068                        // Build target cells rectangle starting from anchor
4069                        let anchor = self
4070                            .graph
4071                            .get_cell_ref(vertex_id)
4072                            .expect("cell ref for vertex");
4073                        let sheet_id = anchor.sheet_id;
4074                        let h = rows.len() as u32;
4075                        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
4076
4077                        // Hard cap to avoid vertex explosion from huge dynamic arrays.
4078                        let spill_cells = (h as u64).saturating_mul(w as u64);
4079                        if spill_cells > self.config.spill.max_spill_cells as u64 {
4080                            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
4081                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
4082                                .with_message("SpillTooLarge")
4083                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
4084                                    expected_rows: h,
4085                                    expected_cols: w,
4086                                });
4087                            let spill_val = LiteralValue::Error(spill_err.clone());
4088                            if let Some(d) = delta.as_deref_mut() {
4089                                let old = self
4090                                    .read_cell_value(
4091                                        self.graph.sheet_name(anchor.sheet_id),
4092                                        anchor.coord.row() + 1,
4093                                        anchor.coord.col() + 1,
4094                                    )
4095                                    .unwrap_or(LiteralValue::Empty);
4096                                if old != spill_val {
4097                                    d.record_cell(
4098                                        anchor.sheet_id,
4099                                        anchor.coord.row(),
4100                                        anchor.coord.col(),
4101                                    );
4102                                }
4103                            }
4104                            self.graph.update_vertex_value(vertex_id, spill_val.clone());
4105                            if self.config.arrow_storage_enabled
4106                                && self.config.delta_overlay_enabled
4107                                && self.config.write_formula_overlay_enabled
4108                            {
4109                                let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
4110                                self.mirror_value_to_computed_overlay(
4111                                    &sheet_name,
4112                                    anchor.coord.row() + 1,
4113                                    anchor.coord.col() + 1,
4114                                    &spill_val,
4115                                );
4116                            }
4117                            return Ok(spill_val);
4118                        }
4119                        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity)
4120                        const PACKED_MAX_ROW: u32 = 1_048_575; // 20-bit max
4121                        const PACKED_MAX_COL: u32 = 16_383; // 14-bit max
4122                        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
4123                        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
4124                        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
4125                            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
4126                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
4127                                .with_message("Spill exceeds sheet bounds")
4128                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
4129                                    expected_rows: h,
4130                                    expected_cols: w,
4131                                });
4132                            let spill_val = LiteralValue::Error(spill_err.clone());
4133                            if let Some(d) = delta.as_deref_mut() {
4134                                let old = self
4135                                    .read_cell_value(
4136                                        self.graph.sheet_name(anchor.sheet_id),
4137                                        anchor.coord.row() + 1,
4138                                        anchor.coord.col() + 1,
4139                                    )
4140                                    .unwrap_or(LiteralValue::Empty);
4141                                if old != spill_val {
4142                                    d.record_cell(
4143                                        anchor.sheet_id,
4144                                        anchor.coord.row(),
4145                                        anchor.coord.col(),
4146                                    );
4147                                }
4148                            }
4149                            self.graph.update_vertex_value(vertex_id, spill_val.clone());
4150                            if self.config.arrow_storage_enabled
4151                                && self.config.delta_overlay_enabled
4152                                && self.config.write_formula_overlay_enabled
4153                            {
4154                                let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
4155                                self.mirror_value_to_computed_overlay(
4156                                    &sheet_name,
4157                                    anchor.coord.row() + 1,
4158                                    anchor.coord.col() + 1,
4159                                    &spill_val,
4160                                );
4161                            }
4162                            return Ok(spill_val);
4163                        }
4164                        let mut targets = Vec::new();
4165                        for r in 0..h {
4166                            for c in 0..w {
4167                                targets.push(self.graph.make_cell_ref_internal(
4168                                    sheet_id,
4169                                    anchor.coord.row() + r,
4170                                    anchor.coord.col() + c,
4171                                ));
4172                            }
4173                        }
4174
4175                        // Plan spill via spill manager shim
4176                        match self.spill_mgr.reserve(
4177                            vertex_id,
4178                            anchor,
4179                            SpillShape { rows: h, cols: w },
4180                            SpillMeta {
4181                                epoch: self.recalc_epoch,
4182                                config: self.config.spill,
4183                            },
4184                        ) {
4185                            Ok(()) => {
4186                                // Commit: write values to grid
4187                                // Default conflict policy is Error + FirstWins; reserve() enforces in-flight locks
4188                                // and plan_spill_region enforces overlap with committed formulas/spills/values.
4189                                if let Err(e) = self.commit_spill_and_mirror(
4190                                    vertex_id,
4191                                    &targets,
4192                                    rows.clone(),
4193                                    delta.as_deref_mut(),
4194                                    None,
4195                                ) {
4196                                    // If commit fails, mark as error
4197                                    self.clear_spill_projection_and_mirror(
4198                                        vertex_id,
4199                                        delta.as_deref_mut(),
4200                                    );
4201                                    if let Some(d) = delta.as_deref_mut() {
4202                                        let old = self
4203                                            .read_cell_value(
4204                                                self.graph.sheet_name(anchor.sheet_id),
4205                                                anchor.coord.row() + 1,
4206                                                anchor.coord.col() + 1,
4207                                            )
4208                                            .unwrap_or(LiteralValue::Empty);
4209                                        let new = LiteralValue::Error(e.clone());
4210                                        if old != new {
4211                                            d.record_cell(
4212                                                anchor.sheet_id,
4213                                                anchor.coord.row(),
4214                                                anchor.coord.col(),
4215                                            );
4216                                        }
4217                                    }
4218                                    let err_val = LiteralValue::Error(e.clone());
4219                                    self.graph.update_vertex_value(vertex_id, err_val.clone());
4220                                    if self.config.arrow_storage_enabled
4221                                        && self.config.delta_overlay_enabled
4222                                        && self.config.write_formula_overlay_enabled
4223                                    {
4224                                        let sheet_name =
4225                                            self.graph.sheet_name(anchor.sheet_id).to_string();
4226                                        self.mirror_value_to_computed_overlay(
4227                                            &sheet_name,
4228                                            anchor.coord.row() + 1,
4229                                            anchor.coord.col() + 1,
4230                                            &err_val,
4231                                        );
4232                                    }
4233                                    return Ok(err_val);
4234                                }
4235                                // Anchor shows the top-left value, like Excel
4236                                let top_left = rows
4237                                    .first()
4238                                    .and_then(|r| r.first())
4239                                    .cloned()
4240                                    .unwrap_or(LiteralValue::Empty);
4241                                self.graph.update_vertex_value(vertex_id, top_left.clone());
4242                                Ok(top_left)
4243                            }
4244                            Err(e) => {
4245                                self.clear_spill_projection_and_mirror(
4246                                    vertex_id,
4247                                    delta.as_deref_mut(),
4248                                );
4249                                let spill_err = ExcelError::new(ExcelErrorKind::Spill)
4250                                    .with_message(
4251                                        e.message.unwrap_or_else(|| "Spill blocked".to_string()),
4252                                    )
4253                                    .with_extra(formualizer_common::ExcelErrorExtra::Spill {
4254                                        expected_rows: h,
4255                                        expected_cols: w,
4256                                    });
4257                                let spill_val = LiteralValue::Error(spill_err.clone());
4258                                if let Some(d) = delta.as_deref_mut() {
4259                                    let old = self
4260                                        .read_cell_value(
4261                                            self.graph.sheet_name(anchor.sheet_id),
4262                                            anchor.coord.row() + 1,
4263                                            anchor.coord.col() + 1,
4264                                        )
4265                                        .unwrap_or(LiteralValue::Empty);
4266                                    if old != spill_val {
4267                                        d.record_cell(
4268                                            anchor.sheet_id,
4269                                            anchor.coord.row(),
4270                                            anchor.coord.col(),
4271                                        );
4272                                    }
4273                                }
4274                                self.graph.update_vertex_value(vertex_id, spill_val.clone());
4275                                if self.config.arrow_storage_enabled
4276                                    && self.config.delta_overlay_enabled
4277                                    && self.config.write_formula_overlay_enabled
4278                                {
4279                                    let sheet_name =
4280                                        self.graph.sheet_name(anchor.sheet_id).to_string();
4281                                    self.mirror_value_to_computed_overlay(
4282                                        &sheet_name,
4283                                        anchor.coord.row() + 1,
4284                                        anchor.coord.col() + 1,
4285                                        &spill_val,
4286                                    );
4287                                }
4288                                Ok(spill_val)
4289                            }
4290                        }
4291                    }
4292                    other => {
4293                        // Scalar result: store value and ensure any previous spill is cleared
4294                        let spill_cells = self
4295                            .graph
4296                            .spill_cells_for_anchor(vertex_id)
4297                            .map(|cells| cells.to_vec())
4298                            .unwrap_or_default();
4299                        if let Some(d) = delta.as_deref_mut()
4300                            && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
4301                        {
4302                            if spill_cells.is_empty() {
4303                                let old = self
4304                                    .read_cell_value(
4305                                        self.graph.sheet_name(anchor.sheet_id),
4306                                        anchor.coord.row() + 1,
4307                                        anchor.coord.col() + 1,
4308                                    )
4309                                    .unwrap_or(LiteralValue::Empty);
4310                                if old != other {
4311                                    d.record_cell(
4312                                        anchor.sheet_id,
4313                                        anchor.coord.row(),
4314                                        anchor.coord.col(),
4315                                    );
4316                                }
4317                            } else {
4318                                for cell in spill_cells.iter() {
4319                                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
4320                                    let old = self
4321                                        .get_cell_value(
4322                                            sheet_name,
4323                                            cell.coord.row() + 1,
4324                                            cell.coord.col() + 1,
4325                                        )
4326                                        .unwrap_or(LiteralValue::Empty);
4327                                    let new = if cell.sheet_id == anchor.sheet_id
4328                                        && cell.coord.row() == anchor.coord.row()
4329                                        && cell.coord.col() == anchor.coord.col()
4330                                    {
4331                                        other.clone()
4332                                    } else {
4333                                        LiteralValue::Empty
4334                                    };
4335                                    Self::record_cell_if_changed(d, cell, &old, &new);
4336                                }
4337                            }
4338                        }
4339                        self.graph.clear_spill_region(vertex_id);
4340                        if self.config.arrow_storage_enabled
4341                            && self.config.delta_overlay_enabled
4342                            && self.config.write_formula_overlay_enabled
4343                        {
4344                            let empty = LiteralValue::Empty;
4345                            for cell in spill_cells.iter() {
4346                                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
4347                                self.mirror_value_to_computed_overlay(
4348                                    &sheet_name,
4349                                    cell.coord.row() + 1,
4350                                    cell.coord.col() + 1,
4351                                    &empty,
4352                                );
4353                            }
4354                        }
4355                        self.graph.update_vertex_value(vertex_id, other.clone());
4356                        // Optionally mirror into Arrow overlay for Arrow-backed reads
4357                        if self.config.arrow_storage_enabled
4358                            && self.config.delta_overlay_enabled
4359                            && self.config.write_formula_overlay_enabled
4360                        {
4361                            let anchor = self
4362                                .graph
4363                                .get_cell_ref(vertex_id)
4364                                .expect("cell ref for vertex");
4365                            let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
4366                            self.mirror_value_to_computed_overlay(
4367                                &sheet_name,
4368                                anchor.coord.row() + 1,
4369                                anchor.coord.col() + 1,
4370                                &other,
4371                            );
4372                        }
4373                        Ok(other)
4374                    }
4375                }
4376            }
4377            Err(e) => {
4378                // Runtime Excel error: store as a cell value instead of propagating
4379                // as an exception so bulk eval paths don't fail the whole pass.
4380                let spill_cells = self
4381                    .graph
4382                    .spill_cells_for_anchor(vertex_id)
4383                    .map(|cells| cells.to_vec())
4384                    .unwrap_or_default();
4385                let err_val = LiteralValue::Error(e.clone());
4386                if let Some(d) = delta
4387                    && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
4388                {
4389                    if spill_cells.is_empty() {
4390                        let old = self
4391                            .read_cell_value(
4392                                self.graph.sheet_name(anchor.sheet_id),
4393                                anchor.coord.row() + 1,
4394                                anchor.coord.col() + 1,
4395                            )
4396                            .unwrap_or(LiteralValue::Empty);
4397                        if old != err_val {
4398                            d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
4399                        }
4400                    } else {
4401                        for cell in spill_cells.iter() {
4402                            let sheet_name = self.graph.sheet_name(cell.sheet_id);
4403                            let old = self
4404                                .get_cell_value(
4405                                    sheet_name,
4406                                    cell.coord.row() + 1,
4407                                    cell.coord.col() + 1,
4408                                )
4409                                .unwrap_or(LiteralValue::Empty);
4410                            let new = if cell.sheet_id == anchor.sheet_id
4411                                && cell.coord.row() == anchor.coord.row()
4412                                && cell.coord.col() == anchor.coord.col()
4413                            {
4414                                err_val.clone()
4415                            } else {
4416                                LiteralValue::Empty
4417                            };
4418                            Self::record_cell_if_changed(d, cell, &old, &new);
4419                        }
4420                    }
4421                }
4422                self.graph.clear_spill_region(vertex_id);
4423                if self.config.arrow_storage_enabled
4424                    && self.config.delta_overlay_enabled
4425                    && self.config.write_formula_overlay_enabled
4426                {
4427                    let empty = LiteralValue::Empty;
4428                    for cell in spill_cells.iter() {
4429                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
4430                        self.mirror_value_to_computed_overlay(
4431                            &sheet_name,
4432                            cell.coord.row() + 1,
4433                            cell.coord.col() + 1,
4434                            &empty,
4435                        );
4436                    }
4437                }
4438                self.graph.update_vertex_value(vertex_id, err_val.clone());
4439                if self.config.arrow_storage_enabled
4440                    && self.config.delta_overlay_enabled
4441                    && self.config.write_formula_overlay_enabled
4442                {
4443                    let anchor = self
4444                        .graph
4445                        .get_cell_ref(vertex_id)
4446                        .expect("cell ref for vertex");
4447                    let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
4448                    self.mirror_value_to_computed_overlay(
4449                        &sheet_name,
4450                        anchor.coord.row() + 1,
4451                        anchor.coord.col() + 1,
4452                        &err_val,
4453                    );
4454                }
4455                Ok(err_val)
4456            }
4457        }
4458    }
4459
4460    fn evaluate_named_scalar(
4461        &mut self,
4462        vertex_id: VertexId,
4463        sheet_id: SheetId,
4464    ) -> Result<LiteralValue, ExcelError> {
4465        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
4466            ExcelError::new(ExcelErrorKind::Name)
4467                .with_message("Named range metadata missing".to_string())
4468        })?;
4469
4470        match &named_range.definition {
4471            NamedDefinition::Cell(cell_ref) => {
4472                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
4473                let row = cell_ref.coord.row() + 1;
4474                let col = cell_ref.coord.col() + 1;
4475
4476                if let Some(dep_vertex) = self.graph.get_vertex_for_cell(cell_ref)
4477                    && matches!(
4478                        self.graph.get_vertex_kind(dep_vertex),
4479                        VertexKind::FormulaScalar | VertexKind::FormulaArray
4480                    )
4481                {
4482                    // Graph does not cache cell/formula values; ensure the precedent is evaluated.
4483                    let value = self.evaluate_vertex(dep_vertex)?;
4484                    self.graph.update_vertex_value(vertex_id, value.clone());
4485                    Ok(value)
4486                } else {
4487                    let value = self
4488                        .get_cell_value(sheet_name, row, col)
4489                        .unwrap_or(LiteralValue::Empty);
4490                    self.graph.update_vertex_value(vertex_id, value.clone());
4491                    Ok(value)
4492                }
4493            }
4494            NamedDefinition::Literal(v) => {
4495                let out = v.clone();
4496                self.graph.update_vertex_value(vertex_id, out.clone());
4497                Ok(out)
4498            }
4499            NamedDefinition::Formula { ast, .. } => {
4500                let context_sheet = match named_range.scope {
4501                    NameScope::Sheet(id) => id,
4502                    NameScope::Workbook => sheet_id,
4503                };
4504                let sheet_name = self.graph.sheet_name(context_sheet);
4505                let cell_ref = self
4506                    .graph
4507                    .get_cell_ref(vertex_id)
4508                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
4509                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
4510                match interpreter.evaluate_ast(ast) {
4511                    Ok(cv) => {
4512                        let value = cv.into_literal();
4513                        match value {
4514                            LiteralValue::Array(_) => {
4515                                let err = ExcelError::new(ExcelErrorKind::NImpl)
4516                                    .with_message("Array result in scalar named range".to_string());
4517                                let err_val = LiteralValue::Error(err.clone());
4518                                self.graph.update_vertex_value(vertex_id, err_val.clone());
4519                                Ok(err_val)
4520                            }
4521                            other => {
4522                                self.graph.update_vertex_value(vertex_id, other.clone());
4523                                Ok(other)
4524                            }
4525                        }
4526                    }
4527                    Err(err) => {
4528                        let err_val = LiteralValue::Error(err.clone());
4529                        self.graph.update_vertex_value(vertex_id, err_val.clone());
4530                        Ok(err_val)
4531                    }
4532                }
4533            }
4534            NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
4535                .with_message("Range-valued name evaluated as scalar".to_string())),
4536        }
4537    }
4538
4539    fn evaluate_named_array(
4540        &mut self,
4541        vertex_id: VertexId,
4542        sheet_id: SheetId,
4543    ) -> Result<LiteralValue, ExcelError> {
4544        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
4545            ExcelError::new(ExcelErrorKind::Name)
4546                .with_message("Named range metadata missing".to_string())
4547        })?;
4548
4549        let out = match &named_range.definition {
4550            NamedDefinition::Range(range_ref) => {
4551                if range_ref.start.sheet_id != range_ref.end.sheet_id {
4552                    return Err(ExcelError::new(ExcelErrorKind::Ref)
4553                        .with_message("Named range cannot span sheets".to_string()));
4554                }
4555
4556                let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
4557                let sr0 = range_ref.start.coord.row();
4558                let sc0 = range_ref.start.coord.col();
4559                let er0 = range_ref.end.coord.row();
4560                let ec0 = range_ref.end.coord.col();
4561                if sr0 > er0 || sc0 > ec0 {
4562                    return Err(ExcelError::new(ExcelErrorKind::Ref)
4563                        .with_message("Invalid named range bounds".to_string()));
4564                }
4565
4566                let h = (er0 - sr0 + 1) as usize;
4567                let w = (ec0 - sc0 + 1) as usize;
4568                let cell_count = (h as u64).saturating_mul(w as u64);
4569                if cell_count > self.config.spill.max_spill_cells as u64 {
4570                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
4571                        "Named range too large to materialize as an array".to_string(),
4572                    ));
4573                }
4574
4575                let mut rows = Vec::with_capacity(h);
4576                for r0 in sr0..=er0 {
4577                    let mut row = Vec::with_capacity(w);
4578                    for c0 in sc0..=ec0 {
4579                        let v = self
4580                            .get_cell_value(sheet_name, r0 + 1, c0 + 1)
4581                            .unwrap_or(LiteralValue::Empty);
4582                        row.push(v);
4583                    }
4584                    rows.push(row);
4585                }
4586                LiteralValue::Array(rows)
4587            }
4588            NamedDefinition::Cell(cell_ref) => {
4589                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
4590                let row = cell_ref.coord.row() + 1;
4591                let col = cell_ref.coord.col() + 1;
4592                let v = self
4593                    .get_cell_value(sheet_name, row, col)
4594                    .unwrap_or(LiteralValue::Empty);
4595                LiteralValue::Array(vec![vec![v]])
4596            }
4597            NamedDefinition::Literal(v) => LiteralValue::Array(vec![vec![v.clone()]]),
4598            NamedDefinition::Formula { ast, .. } => {
4599                let context_sheet = match named_range.scope {
4600                    NameScope::Sheet(id) => id,
4601                    NameScope::Workbook => sheet_id,
4602                };
4603                let sheet_name = self.graph.sheet_name(context_sheet);
4604                let cell_ref = self
4605                    .graph
4606                    .get_cell_ref(vertex_id)
4607                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
4608                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
4609                match interpreter.evaluate_ast(ast) {
4610                    Ok(cv) => {
4611                        let v = cv.into_literal();
4612                        match v {
4613                            LiteralValue::Array(_) => v,
4614                            other => LiteralValue::Array(vec![vec![other]]),
4615                        }
4616                    }
4617                    Err(err) => LiteralValue::Error(err),
4618                }
4619            }
4620        };
4621
4622        self.graph.update_vertex_value(vertex_id, out.clone());
4623        Ok(out)
4624    }
4625
4626    /// Evaluate only the necessary precedents for specific target cells (demand-driven)
4627    pub fn evaluate_until(
4628        &mut self,
4629        targets: &[(&str, u32, u32)],
4630    ) -> Result<EvalResult, ExcelError> {
4631        #[cfg(feature = "tracing")]
4632        let _span_eval = tracing::info_span!("evaluate_until", targets = targets.len()).entered();
4633        let start = crate::instant::FzInstant::now();
4634        let _source_cache = self.source_cache_session();
4635
4636        // Parse target cell addresses
4637        let mut target_addrs = Vec::new();
4638        for (sheet, row, col) in targets {
4639            // For now, assume simple A1-style references on default sheet
4640            // TODO: Parse complex references with sheets
4641            let sheet_id = self.graph.sheet_id_mut(sheet);
4642            let coord = Coord::from_excel(*row, *col, true, true);
4643            target_addrs.push(CellRef::new(sheet_id, coord));
4644        }
4645
4646        // Find vertex IDs for targets
4647        let mut target_vertex_ids = Vec::new();
4648        for addr in &target_addrs {
4649            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
4650                target_vertex_ids.push(*vertex_id);
4651            }
4652        }
4653
4654        if target_vertex_ids.is_empty() {
4655            return Ok(EvalResult {
4656                computed_vertices: 0,
4657                cycle_errors: 0,
4658                elapsed: start.elapsed(),
4659            });
4660        }
4661
4662        // Build demand subgraph with virtual edges for compressed ranges
4663        #[cfg(feature = "tracing")]
4664        let _span_sub = tracing::info_span!("demand_subgraph_build").entered();
4665        let (precedents_to_eval, vdeps) = self.build_demand_subgraph(&target_vertex_ids);
4666        #[cfg(feature = "tracing")]
4667        drop(_span_sub);
4668
4669        if precedents_to_eval.is_empty() {
4670            return Ok(EvalResult {
4671                computed_vertices: 0,
4672                cycle_errors: 0,
4673                elapsed: start.elapsed(),
4674            });
4675        }
4676
4677        // Create schedule for the minimal subgraph, honoring virtual edges
4678        let scheduler = Scheduler::new(&self.graph);
4679        #[cfg(feature = "tracing")]
4680        let _span_sched =
4681            tracing::info_span!("schedule_build", vertices = precedents_to_eval.len()).entered();
4682        let schedule = scheduler.create_schedule_with_virtual(&precedents_to_eval, &vdeps)?;
4683        #[cfg(feature = "tracing")]
4684        drop(_span_sched);
4685
4686        // Handle cycles first
4687        let mut cycle_errors = 0;
4688        for cycle in &schedule.cycles {
4689            cycle_errors += 1;
4690            let circ_error = LiteralValue::Error(
4691                ExcelError::new(ExcelErrorKind::Circ)
4692                    .with_message("Circular dependency detected".to_string()),
4693            );
4694            for &vertex_id in cycle {
4695                self.graph
4696                    .update_vertex_value(vertex_id, circ_error.clone());
4697                self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
4698            }
4699        }
4700
4701        // Evaluate layers (parallel when enabled, mirroring evaluate_all)
4702        let mut computed_vertices = 0;
4703        for layer in &schedule.layers {
4704            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
4705                computed_vertices += self.evaluate_layer_parallel(layer)?;
4706            } else {
4707                computed_vertices += self.evaluate_layer_sequential(layer)?;
4708            }
4709        }
4710
4711        // Clear warmup context at end of evaluation
4712
4713        // Clear dirty flags for evaluated vertices
4714        self.graph.clear_dirty_flags(&precedents_to_eval);
4715
4716        // Re-dirty volatile vertices
4717        self.graph.redirty_volatiles();
4718
4719        Ok(EvalResult {
4720            computed_vertices,
4721            cycle_errors,
4722            elapsed: start.elapsed(),
4723        })
4724    }
4725
4726    fn evaluate_until_with_delta_collector(
4727        &mut self,
4728        targets: &[(&str, u32, u32)],
4729        delta: &mut DeltaCollector,
4730    ) -> Result<EvalResult, ExcelError> {
4731        #[cfg(feature = "tracing")]
4732        let _span_eval =
4733            tracing::info_span!("evaluate_until_with_delta", targets = targets.len()).entered();
4734        let start = crate::instant::FzInstant::now();
4735        let _source_cache = self.source_cache_session();
4736
4737        let mut target_addrs = Vec::new();
4738        for (sheet, row, col) in targets {
4739            let sheet_id = self.graph.sheet_id_mut(sheet);
4740            let coord = Coord::from_excel(*row, *col, true, true);
4741            target_addrs.push(CellRef::new(sheet_id, coord));
4742        }
4743
4744        let mut target_vertex_ids = Vec::new();
4745        for addr in &target_addrs {
4746            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
4747                target_vertex_ids.push(*vertex_id);
4748            }
4749        }
4750
4751        if target_vertex_ids.is_empty() {
4752            return Ok(EvalResult {
4753                computed_vertices: 0,
4754                cycle_errors: 0,
4755                elapsed: start.elapsed(),
4756            });
4757        }
4758
4759        let (precedents_to_eval, vdeps) = self.build_demand_subgraph(&target_vertex_ids);
4760
4761        if precedents_to_eval.is_empty() {
4762            return Ok(EvalResult {
4763                computed_vertices: 0,
4764                cycle_errors: 0,
4765                elapsed: start.elapsed(),
4766            });
4767        }
4768
4769        let scheduler = Scheduler::new(&self.graph);
4770        let schedule = scheduler.create_schedule_with_virtual(&precedents_to_eval, &vdeps)?;
4771
4772        let mut cycle_errors = 0;
4773        let circ_error = LiteralValue::Error(
4774            ExcelError::new(ExcelErrorKind::Circ)
4775                .with_message("Circular dependency detected".to_string()),
4776        );
4777        for cycle in &schedule.cycles {
4778            cycle_errors += 1;
4779            for &vertex_id in cycle {
4780                if delta.mode != DeltaMode::Off
4781                    && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
4782                {
4783                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
4784                    let old = self
4785                        .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
4786                        .unwrap_or(LiteralValue::Empty);
4787                    if old != circ_error {
4788                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
4789                    }
4790                }
4791                self.graph
4792                    .update_vertex_value(vertex_id, circ_error.clone());
4793                self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
4794            }
4795        }
4796
4797        let mut computed_vertices = 0;
4798        for layer in &schedule.layers {
4799            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
4800                computed_vertices += self.evaluate_layer_parallel_with_delta(layer, delta)?;
4801            } else {
4802                computed_vertices += self.evaluate_layer_sequential_with_delta(layer, delta)?;
4803            }
4804        }
4805
4806        self.graph.clear_dirty_flags(&precedents_to_eval);
4807        self.graph.redirty_volatiles();
4808
4809        Ok(EvalResult {
4810            computed_vertices,
4811            cycle_errors,
4812            elapsed: start.elapsed(),
4813        })
4814    }
4815
4816    /// Build a reusable evaluation plan that covers every formula vertex in the workbook.
4817    pub fn build_recalc_plan(&self) -> Result<RecalcPlan, ExcelError> {
4818        let mut vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
4819        vertices.sort_unstable();
4820        if vertices.is_empty() {
4821            return Ok(RecalcPlan {
4822                schedule: crate::engine::Schedule {
4823                    layers: Vec::new(),
4824                    cycles: Vec::new(),
4825                },
4826                has_dynamic_refs: false,
4827            });
4828        }
4829
4830        let has_dynamic_refs = vertices.iter().copied().any(|v| self.graph.is_dynamic(v));
4831        let (schedule, _, _) = self.create_evaluation_schedule_uncached(&vertices)?;
4832        Ok(RecalcPlan {
4833            schedule,
4834            has_dynamic_refs,
4835        })
4836    }
4837
4838    /// Evaluate using a previously constructed plan. This avoids rebuilding layer schedules for each run.
4839    pub fn evaluate_recalc_plan(&mut self, plan: &RecalcPlan) -> Result<EvalResult, ExcelError> {
4840        let _source_cache = self.source_cache_session();
4841        self.validate_deterministic_mode()?;
4842        if self.config.defer_graph_building {
4843            self.build_graph_all()?;
4844        }
4845
4846        let start = crate::instant::FzInstant::now();
4847        let dirty_vertices = self.graph.get_evaluation_vertices();
4848        if dirty_vertices.is_empty() {
4849            return Ok(EvalResult {
4850                computed_vertices: 0,
4851                cycle_errors: 0,
4852                elapsed: start.elapsed(),
4853            });
4854        }
4855
4856        // Dynamic-reference formulas (INDIRECT/OFFSET-class) require per-pass virtual-dep
4857        // augmentation. Reuse the direct recalc flow to preserve semantic parity.
4858        if plan.has_dynamic_refs {
4859            self.virtual_dep_fallback_activations =
4860                self.virtual_dep_fallback_activations.saturating_add(1);
4861            return self.evaluate_all();
4862        }
4863
4864        let dirty_set: FxHashSet<VertexId> = dirty_vertices.iter().copied().collect();
4865        let mut computed_vertices = 0;
4866        let mut cycle_errors = 0;
4867
4868        if !plan.schedule.cycles.is_empty() {
4869            let circ_error = LiteralValue::Error(
4870                ExcelError::new(ExcelErrorKind::Circ)
4871                    .with_message("Circular dependency detected".to_string()),
4872            );
4873            for cycle in &plan.schedule.cycles {
4874                if !cycle.iter().any(|v| dirty_set.contains(v)) {
4875                    continue;
4876                }
4877                cycle_errors += 1;
4878                for &vertex_id in cycle {
4879                    if dirty_set.contains(&vertex_id) {
4880                        self.graph
4881                            .update_vertex_value(vertex_id, circ_error.clone());
4882                        self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
4883                    }
4884                }
4885            }
4886        }
4887
4888        for layer in &plan.schedule.layers {
4889            let work: Vec<VertexId> = layer
4890                .vertices
4891                .iter()
4892                .copied()
4893                .filter(|v| dirty_set.contains(v))
4894                .collect();
4895            if work.is_empty() {
4896                continue;
4897            }
4898            let temp_layer = crate::engine::scheduler::Layer { vertices: work };
4899            if self.thread_pool.is_some() && temp_layer.vertices.len() > 1 {
4900                computed_vertices += self.evaluate_layer_parallel(&temp_layer)?;
4901            } else {
4902                computed_vertices += self.evaluate_layer_sequential(&temp_layer)?;
4903            }
4904        }
4905
4906        self.graph.clear_dirty_flags(&dirty_vertices);
4907        self.graph.redirty_volatiles();
4908
4909        Ok(EvalResult {
4910            computed_vertices,
4911            cycle_errors,
4912            elapsed: start.elapsed(),
4913        })
4914    }
4915    /// Evaluate all dirty/volatile vertices
4916    pub fn evaluate_all(&mut self) -> Result<EvalResult, ExcelError> {
4917        let _source_cache = self.source_cache_session();
4918        self.validate_deterministic_mode()?;
4919        if self.config.defer_graph_building {
4920            // Build graph for all staged formulas before evaluating
4921            self.build_graph_all()?;
4922        }
4923        self.reset_virtual_dep_telemetry_if_disabled();
4924        #[cfg(feature = "tracing")]
4925        let _span_eval = tracing::info_span!("evaluate_all").entered();
4926        let start = crate::instant::FzInstant::now();
4927        let mut computed_vertices = 0;
4928        let mut cycle_errors = 0;
4929        let mut replan_iterations = 0;
4930        const MAX_REPLAN: usize = 5;
4931        let mut telemetry = self
4932            .config
4933            .enable_virtual_dep_telemetry
4934            .then(|| self.start_virtual_dep_telemetry());
4935
4936        loop {
4937            let to_evaluate = self.graph.get_evaluation_vertices();
4938            if to_evaluate.is_empty() {
4939                if let Some(t) = telemetry.as_mut()
4940                    && t.bailout_reason.is_none()
4941                {
4942                    t.bailout_reason = Some("no_work");
4943                }
4944                break;
4945            }
4946
4947            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
4948            if let Some(t) = telemetry.as_mut() {
4949                Self::accumulate_schedule_meta(t, &meta);
4950            }
4951
4952            // Handle cycles first by marking them with #CIRC!
4953            for cycle in &schedule.cycles {
4954                cycle_errors += 1;
4955                let circ_error = LiteralValue::Error(
4956                    ExcelError::new(ExcelErrorKind::Circ)
4957                        .with_message("Circular dependency detected".to_string()),
4958                );
4959                for &vertex_id in cycle {
4960                    self.graph
4961                        .update_vertex_value(vertex_id, circ_error.clone());
4962                    self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
4963                }
4964            }
4965
4966            // Evaluate acyclic layers (parallel or sequential based on config)
4967            for layer in &schedule.layers {
4968                if self.thread_pool.is_some() && layer.vertices.len() > 1 {
4969                    computed_vertices += self.evaluate_layer_parallel(layer)?;
4970                } else {
4971                    computed_vertices += self.evaluate_layer_sequential(layer)?;
4972                }
4973            }
4974
4975            // Check if dynamic dependencies changed
4976            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
4977            if let Some(t) = telemetry.as_mut() {
4978                t.changed_vdeps_total += changed_vertices.len();
4979            }
4980
4981            self.graph.clear_dirty_flags(&to_evaluate);
4982            for v in &changed_vertices {
4983                self.graph.set_dirty(*v, true);
4984            }
4985
4986            if changed_vertices.is_empty() {
4987                if let Some(t) = telemetry.as_mut() {
4988                    t.bailout_reason = Some("converged");
4989                }
4990                break;
4991            }
4992            if replan_iterations >= MAX_REPLAN {
4993                if let Some(t) = telemetry.as_mut() {
4994                    t.bailout_reason = Some("max_replan");
4995                }
4996                break;
4997            }
4998
4999            replan_iterations += 1;
5000        }
5001
5002        if let Some(mut t) = telemetry {
5003            t.replan_iterations = replan_iterations;
5004            self.last_virtual_dep_telemetry = t;
5005        }
5006
5007        // Re-dirty volatile vertices for the next evaluation cycle
5008        self.graph.redirty_volatiles();
5009
5010        // Advance recalc epoch after a full evaluation pass finishes
5011        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
5012
5013        Ok(EvalResult {
5014            computed_vertices,
5015            cycle_errors,
5016            elapsed: start.elapsed(),
5017        })
5018    }
5019
5020    pub fn evaluate_all_with_delta(&mut self) -> Result<(EvalResult, EvalDelta), ExcelError> {
5021        let mut collector = DeltaCollector::new(DeltaMode::Cells);
5022        let result = self.evaluate_all_with_delta_collector(&mut collector)?;
5023        Ok((result, collector.finish()))
5024    }
5025
5026    fn evaluate_all_with_delta_collector(
5027        &mut self,
5028        delta: &mut DeltaCollector,
5029    ) -> Result<EvalResult, ExcelError> {
5030        let _source_cache = self.source_cache_session();
5031        if self.config.defer_graph_building {
5032            self.build_graph_all()?;
5033        }
5034        self.reset_virtual_dep_telemetry_if_disabled();
5035        #[cfg(feature = "tracing")]
5036        let _span_eval = tracing::info_span!("evaluate_all_with_delta").entered();
5037        let start = crate::instant::FzInstant::now();
5038        let mut computed_vertices = 0;
5039        let mut cycle_errors = 0;
5040
5041        let mut replan_iterations = 0;
5042        const MAX_REPLAN: usize = 5;
5043        let mut telemetry = self
5044            .config
5045            .enable_virtual_dep_telemetry
5046            .then(|| self.start_virtual_dep_telemetry());
5047
5048        loop {
5049            let to_evaluate = self.graph.get_evaluation_vertices();
5050            if to_evaluate.is_empty() {
5051                if let Some(t) = telemetry.as_mut()
5052                    && t.bailout_reason.is_none()
5053                {
5054                    t.bailout_reason = Some("no_work");
5055                }
5056                break;
5057            }
5058
5059            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
5060            if let Some(t) = telemetry.as_mut() {
5061                Self::accumulate_schedule_meta(t, &meta);
5062            }
5063
5064            let circ_error = LiteralValue::Error(
5065                ExcelError::new(ExcelErrorKind::Circ)
5066                    .with_message("Circular dependency detected".to_string()),
5067            );
5068            for cycle in &schedule.cycles {
5069                cycle_errors += 1;
5070                for &vertex_id in cycle {
5071                    if delta.mode != DeltaMode::Off
5072                        && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
5073                    {
5074                        let sheet_name = self.graph.sheet_name(cell.sheet_id);
5075                        let old = self
5076                            .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
5077                            .unwrap_or(LiteralValue::Empty);
5078                        if old != circ_error {
5079                            delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
5080                        }
5081                    }
5082                    self.graph
5083                        .update_vertex_value(vertex_id, circ_error.clone());
5084                    self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
5085                }
5086            }
5087
5088            for layer in &schedule.layers {
5089                if self.thread_pool.is_some() && layer.vertices.len() > 1 {
5090                    computed_vertices += self.evaluate_layer_parallel_with_delta(layer, delta)?;
5091                } else {
5092                    computed_vertices += self.evaluate_layer_sequential_with_delta(layer, delta)?;
5093                }
5094            }
5095
5096            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
5097            if let Some(t) = telemetry.as_mut() {
5098                t.changed_vdeps_total += changed_vertices.len();
5099            }
5100            self.graph.clear_dirty_flags(&to_evaluate);
5101            for v in &changed_vertices {
5102                self.graph.set_dirty(*v, true);
5103            }
5104
5105            if changed_vertices.is_empty() {
5106                if let Some(t) = telemetry.as_mut() {
5107                    t.bailout_reason = Some("converged");
5108                }
5109                break;
5110            }
5111            if replan_iterations >= MAX_REPLAN {
5112                if let Some(t) = telemetry.as_mut() {
5113                    t.bailout_reason = Some("max_replan");
5114                }
5115                break;
5116            }
5117            replan_iterations += 1;
5118        }
5119
5120        if let Some(mut t) = telemetry {
5121            t.replan_iterations = replan_iterations;
5122            self.last_virtual_dep_telemetry = t;
5123        }
5124
5125        self.graph.redirty_volatiles();
5126        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
5127
5128        Ok(EvalResult {
5129            computed_vertices,
5130            cycle_errors,
5131            elapsed: start.elapsed(),
5132        })
5133    }
5134
5135    /// Convenience: demand-driven evaluation of a single cell by sheet name and row/col.
5136    ///
5137    /// This will evaluate only the minimal set of dirty / volatile precedents required
5138    /// to bring the target cell up-to-date (as if a user asked for that single value),
5139    /// rather than scheduling a full workbook recalc. If the cell is already clean and
5140    /// non-volatile, no vertices will be recomputed.
5141    ///
5142    /// Returns the (possibly newly computed) value stored for the cell afterwards.
5143    /// Empty cells return None. Errors are surfaced via the Result type.
5144    pub fn evaluate_cell(
5145        &mut self,
5146        sheet: &str,
5147        row: u32,
5148        col: u32,
5149    ) -> Result<Option<LiteralValue>, ExcelError> {
5150        if row == 0 || col == 0 {
5151            return Err(ExcelError::new(ExcelErrorKind::Ref)
5152                .with_message("Row and column must be >= 1".to_string()));
5153        }
5154
5155        if self.config.defer_graph_building {
5156            self.build_graph_for_sheets(std::iter::once(sheet))?;
5157        }
5158
5159        let result = self.evaluate_cells(&[(sheet, row, col)])?;
5160
5161        match result.len() {
5162            0 => Ok(None),
5163            1 => {
5164                let v = result.into_iter().next().unwrap();
5165                Ok(v)
5166            }
5167            _ => unreachable!("evaluate_cells returned unexpected length"),
5168        }
5169    }
5170
5171    /// Convenience: demand-driven evaluation of multiple cells; accepts a slice of
5172    /// (sheet, row, col) triples. The union of required dirty / volatile precedents
5173    /// is computed once and evaluated, which is typically faster than calling
5174    /// `evaluate_cell` repeatedly for a related set of targets.
5175    ///
5176    /// Returns the resulting values for each requested target in the same order.
5177    pub fn evaluate_cells(
5178        &mut self,
5179        targets: &[(&str, u32, u32)],
5180    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
5181        self.validate_deterministic_mode()?;
5182        if targets.is_empty() {
5183            return Ok(Vec::new());
5184        }
5185        if self.config.defer_graph_building {
5186            let mut sheets: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::default();
5187            for (s, _, _) in targets.iter() {
5188                sheets.insert(*s);
5189            }
5190            self.build_graph_for_sheets(sheets.iter().cloned())?;
5191        }
5192        self.evaluate_until(targets)?;
5193        Ok(targets
5194            .iter()
5195            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
5196            .collect())
5197    }
5198
5199    pub fn evaluate_cells_cancellable(
5200        &mut self,
5201        targets: &[(&str, u32, u32)],
5202        cancel_flag: Arc<AtomicBool>,
5203    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
5204        self.active_cancel_flag = Some(cancel_flag.clone());
5205        let res = self.evaluate_cells_cancellable_impl(targets, &cancel_flag);
5206        self.active_cancel_flag = None;
5207        res
5208    }
5209
5210    fn evaluate_cells_cancellable_impl(
5211        &mut self,
5212        targets: &[(&str, u32, u32)],
5213        cancel_flag: &AtomicBool,
5214    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
5215        self.validate_deterministic_mode()?;
5216        if targets.is_empty() {
5217            return Ok(Vec::new());
5218        }
5219        if self.config.defer_graph_building {
5220            let mut sheets: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::default();
5221            for (s, _, _) in targets.iter() {
5222                sheets.insert(*s);
5223            }
5224            self.build_graph_for_sheets(sheets.iter().cloned())?;
5225        }
5226
5227        // evaluate_until_cancellable takes &[&str] in A1 notation, but we have (&str, u32, u32)
5228        // Let's implement evaluate_until_coords_cancellable or similar, or just convert
5229        let a1_targets: Vec<String> = targets
5230            .iter()
5231            .map(|(s, r, c)| {
5232                format!("{}!{}", s, col_letters_from_1based(*c).unwrap()) + &r.to_string()
5233            })
5234            .collect();
5235        let a1_refs: Vec<&str> = a1_targets.iter().map(|s| s.as_str()).collect();
5236
5237        self.evaluate_until_cancellable_impl(&a1_refs, cancel_flag)?;
5238
5239        Ok(targets
5240            .iter()
5241            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
5242            .collect())
5243    }
5244
5245    pub fn evaluate_cells_with_delta(
5246        &mut self,
5247        targets: &[(&str, u32, u32)],
5248    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
5249        self.validate_deterministic_mode()?;
5250        if targets.is_empty() {
5251            return Ok((Vec::new(), EvalDelta::default()));
5252        }
5253        if self.config.defer_graph_building {
5254            let mut sheets: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::default();
5255            for (s, _, _) in targets.iter() {
5256                sheets.insert(*s);
5257            }
5258            self.build_graph_for_sheets(sheets.iter().cloned())?;
5259        }
5260        let mut collector = DeltaCollector::new(DeltaMode::Cells);
5261        self.evaluate_until_with_delta_collector(targets, &mut collector)?;
5262        let values = targets
5263            .iter()
5264            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
5265            .collect();
5266        Ok((values, collector.finish()))
5267    }
5268
5269    /// Get the evaluation plan for target cells without actually evaluating them
5270    pub fn get_eval_plan(&self, targets: &[(&str, u32, u32)]) -> Result<EvalPlan, ExcelError> {
5271        if targets.is_empty() {
5272            return Ok(EvalPlan {
5273                total_vertices_to_evaluate: 0,
5274                layers: Vec::new(),
5275                cycles_detected: 0,
5276                dirty_count: 0,
5277                volatile_count: 0,
5278                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
5279                estimated_parallel_layers: 0,
5280                target_cells: Vec::new(),
5281            });
5282        }
5283        if self.config.defer_graph_building {
5284            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
5285                "Evaluation plan requested with deferred graph; build first or call evaluate_*",
5286            ));
5287        }
5288
5289        // Convert targets to A1 notation for consistency
5290        let addresses: Vec<String> = targets
5291            .iter()
5292            .map(|(s, r, c)| format!("{}!{}{}", s, Self::col_to_letters(*c), r))
5293            .collect();
5294
5295        // Parse target cell addresses
5296        let mut target_addrs = Vec::new();
5297        for (sheet, row, col) in targets {
5298            if let Some(sheet_id) = self.graph.sheet_id(sheet) {
5299                let coord = Coord::from_excel(*row, *col, true, true);
5300                target_addrs.push(CellRef::new(sheet_id, coord));
5301            }
5302        }
5303
5304        // Find vertex IDs for targets
5305        let mut target_vertex_ids = Vec::new();
5306        for addr in &target_addrs {
5307            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
5308                target_vertex_ids.push(*vertex_id);
5309            }
5310        }
5311
5312        if target_vertex_ids.is_empty() {
5313            return Ok(EvalPlan {
5314                total_vertices_to_evaluate: 0,
5315                layers: Vec::new(),
5316                cycles_detected: 0,
5317                dirty_count: 0,
5318                volatile_count: 0,
5319                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
5320                estimated_parallel_layers: 0,
5321                target_cells: addresses,
5322            });
5323        }
5324
5325        // Build demand subgraph with virtual edges (same as evaluate_until)
5326        let (precedents_to_eval, vdeps) = self.build_demand_subgraph(&target_vertex_ids);
5327
5328        if precedents_to_eval.is_empty() {
5329            return Ok(EvalPlan {
5330                total_vertices_to_evaluate: 0,
5331                layers: Vec::new(),
5332                cycles_detected: 0,
5333                dirty_count: 0,
5334                volatile_count: 0,
5335                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
5336                estimated_parallel_layers: 0,
5337                target_cells: addresses,
5338            });
5339        }
5340
5341        // Count dirty and volatile vertices
5342        let mut dirty_count = 0;
5343        let mut volatile_count = 0;
5344        for &vertex_id in &precedents_to_eval {
5345            if self.graph.is_dirty(vertex_id) {
5346                dirty_count += 1;
5347            }
5348            if self.graph.is_volatile(vertex_id) {
5349                volatile_count += 1;
5350            }
5351        }
5352
5353        // Create schedule for the minimal subgraph honoring virtual edges
5354        let scheduler = Scheduler::new(&self.graph);
5355        let schedule = scheduler.create_schedule_with_virtual(&precedents_to_eval, &vdeps)?;
5356
5357        // Build layer information
5358        let mut layers = Vec::new();
5359        let mut estimated_parallel_layers = 0;
5360        let parallel_enabled = self.config.enable_parallel && self.thread_pool.is_some();
5361
5362        for layer in &schedule.layers {
5363            let parallel_eligible = parallel_enabled && layer.vertices.len() > 1;
5364            if parallel_eligible {
5365                estimated_parallel_layers += 1;
5366            }
5367
5368            // Get sample cell addresses (up to 5)
5369            let sample_cells: Vec<String> = layer
5370                .vertices
5371                .iter()
5372                .take(5)
5373                .filter_map(|&vertex_id| {
5374                    self.graph
5375                        .get_cell_ref_for_vertex(vertex_id)
5376                        .map(|cell_ref| {
5377                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
5378                            format!(
5379                                "{}!{}{}",
5380                                sheet_name,
5381                                Self::col_to_letters(cell_ref.coord.col()),
5382                                cell_ref.coord.row() + 1
5383                            )
5384                        })
5385                })
5386                .collect();
5387
5388            layers.push(LayerInfo {
5389                vertex_count: layer.vertices.len(),
5390                parallel_eligible,
5391                sample_cells,
5392            });
5393        }
5394
5395        Ok(EvalPlan {
5396            total_vertices_to_evaluate: precedents_to_eval.len(),
5397            layers,
5398            cycles_detected: schedule.cycles.len(),
5399            dirty_count,
5400            volatile_count,
5401            parallel_enabled,
5402            estimated_parallel_layers,
5403            target_cells: addresses,
5404        })
5405    }
5406    /// Helper to create a schedule, integrating virtual dependencies automatically.
5407    fn create_evaluation_schedule(
5408        &mut self,
5409        to_evaluate: &[VertexId],
5410    ) -> Result<ScheduleBuildOutput, ExcelError> {
5411        if self.can_use_static_schedule_cache(to_evaluate) {
5412            if let Some(cached) = self.cached_static_schedule.as_ref()
5413                && cached.topology_epoch == self.topology_epoch
5414                && cached.candidate_vertices.as_slice() == to_evaluate
5415            {
5416                let meta = ScheduleBuildMeta {
5417                    candidate_vertices: to_evaluate.len(),
5418                    vdeps_vertices: 0,
5419                    vdeps_edges: 0,
5420                    builder_elapsed_ms: 0,
5421                    used_virtual_schedule: false,
5422                    schedule_cache_hit: true,
5423                    schedule_cache_eligible: true,
5424                };
5425                return Ok((cached.schedule.clone(), FxHashMap::default(), meta));
5426            }
5427
5428            let (schedule, vdeps, mut meta) =
5429                self.create_evaluation_schedule_uncached(to_evaluate)?;
5430            meta.schedule_cache_hit = false;
5431            meta.schedule_cache_eligible = true;
5432            if vdeps.is_empty() {
5433                self.cached_static_schedule = Some(CachedScheduleEntry {
5434                    topology_epoch: self.topology_epoch,
5435                    candidate_vertices: to_evaluate.to_vec(),
5436                    schedule: schedule.clone(),
5437                });
5438            }
5439            return Ok((schedule, vdeps, meta));
5440        }
5441
5442        let (schedule, vdeps, mut meta) = self.create_evaluation_schedule_uncached(to_evaluate)?;
5443        meta.schedule_cache_hit = false;
5444        meta.schedule_cache_eligible = false;
5445        Ok((schedule, vdeps, meta))
5446    }
5447
5448    fn create_evaluation_schedule_uncached(
5449        &self,
5450        to_evaluate: &[VertexId],
5451    ) -> Result<ScheduleBuildOutput, ExcelError> {
5452        let builder = VirtualDepBuilder::new(self);
5453        let (vdeps, augmented, builder_elapsed_ms, vdeps_edges) =
5454            if self.config.enable_virtual_dep_telemetry {
5455                let build_started = crate::instant::FzInstant::now();
5456                let (vdeps, augmented) = builder.build(to_evaluate);
5457                let builder_elapsed_ms = build_started.elapsed().as_millis();
5458                let vdeps_edges = vdeps.values().map(|deps| deps.len()).sum::<usize>();
5459                (vdeps, augmented, builder_elapsed_ms, vdeps_edges)
5460            } else {
5461                let (vdeps, augmented) = builder.build(to_evaluate);
5462                (vdeps, augmented, 0, 0)
5463            };
5464
5465        let mut final_evaluate = to_evaluate.to_vec();
5466        if !augmented.is_empty() {
5467            final_evaluate.extend(augmented);
5468            final_evaluate.sort_unstable();
5469            final_evaluate.dedup();
5470        }
5471
5472        let use_virtual = !vdeps.is_empty();
5473
5474        let scheduler = Scheduler::new(&self.graph);
5475        let schedule = if use_virtual {
5476            scheduler.create_schedule_with_virtual(&final_evaluate, &vdeps)?
5477        } else {
5478            scheduler.create_schedule(&final_evaluate)?
5479        };
5480
5481        let meta = ScheduleBuildMeta {
5482            candidate_vertices: to_evaluate.len(),
5483            vdeps_vertices: vdeps.len(),
5484            vdeps_edges,
5485            builder_elapsed_ms,
5486            used_virtual_schedule: use_virtual,
5487            schedule_cache_hit: false,
5488            schedule_cache_eligible: false,
5489        };
5490
5491        Ok((schedule, vdeps, meta))
5492    }
5493
5494    fn can_use_static_schedule_cache(&self, to_evaluate: &[VertexId]) -> bool {
5495        !to_evaluate.is_empty()
5496            && to_evaluate.iter().copied().all(|v| {
5497                !self.graph.is_dynamic(v) && self.graph.get_range_dependencies(v).is_none()
5498            })
5499    }
5500
5501    fn start_virtual_dep_telemetry(&self) -> VirtualDepTelemetry {
5502        VirtualDepTelemetry {
5503            fallback_mode_activations: self.virtual_dep_fallback_activations,
5504            ..VirtualDepTelemetry::default()
5505        }
5506    }
5507
5508    fn accumulate_schedule_meta(telemetry: &mut VirtualDepTelemetry, meta: &ScheduleBuildMeta) {
5509        telemetry.candidate_vertices_total += meta.candidate_vertices;
5510        telemetry.vdeps_vertices_total += meta.vdeps_vertices;
5511        telemetry.vdeps_edges_total += meta.vdeps_edges;
5512        telemetry.builder_elapsed_ms_total += meta.builder_elapsed_ms;
5513        if meta.schedule_cache_eligible {
5514            if meta.schedule_cache_hit {
5515                telemetry.schedule_cache_hits += 1;
5516                telemetry.reused_schedule_vertices_total += meta.candidate_vertices;
5517            } else {
5518                telemetry.schedule_cache_misses += 1;
5519            }
5520        }
5521        if meta.used_virtual_schedule {
5522            telemetry.schedule_virtual_passes += 1;
5523        } else {
5524            telemetry.schedule_static_passes += 1;
5525        }
5526    }
5527
5528    fn changed_virtual_dep_vertices(
5529        &self,
5530        to_evaluate: &[VertexId],
5531        old_vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
5532    ) -> Vec<VertexId> {
5533        if !to_evaluate
5534            .iter()
5535            .copied()
5536            .any(|v| self.graph.is_dynamic(v))
5537        {
5538            return Vec::new();
5539        }
5540
5541        let builder = VirtualDepBuilder::new(self);
5542        let (new_vdeps, _) = builder.build(to_evaluate);
5543
5544        let mut candidates = FxHashSet::default();
5545        candidates.extend(old_vdeps.keys().copied());
5546        candidates.extend(new_vdeps.keys().copied());
5547
5548        let mut changed = Vec::new();
5549        for v in candidates {
5550            if old_vdeps.get(&v) != new_vdeps.get(&v) {
5551                changed.push(v);
5552            }
5553        }
5554        changed
5555    }
5556
5557    /// Build a demand-driven subgraph for the given targets, including ephemeral edges for
5558    /// compressed ranges, and returning the set of dirty/volatile precedents and virtual deps.
5559    fn build_demand_subgraph(
5560        &self,
5561        target_vertices: &[VertexId],
5562    ) -> (
5563        Vec<VertexId>,
5564        rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
5565    ) {
5566        #[cfg(feature = "tracing")]
5567        let _span =
5568            tracing::info_span!("demand_subgraph", targets = target_vertices.len()).entered();
5569        use rustc_hash::{FxHashMap, FxHashSet};
5570
5571        let mut to_evaluate: FxHashSet<VertexId> = FxHashSet::default();
5572        let mut visited: FxHashSet<VertexId> = FxHashSet::default();
5573        let mut stack: Vec<VertexId> = Vec::new();
5574        let mut vdeps: FxHashMap<VertexId, Vec<VertexId>> = FxHashMap::default(); // incoming deps per vertex
5575
5576        for &t in target_vertices {
5577            stack.push(t);
5578        }
5579
5580        while let Some(v) = stack.pop() {
5581            if !visited.insert(v) {
5582                continue;
5583            }
5584            if !self.graph.vertex_exists(v) {
5585                continue;
5586            }
5587            // Only schedule dirty/volatile formulas
5588            match self.graph.get_vertex_kind(v) {
5589                VertexKind::FormulaScalar | VertexKind::FormulaArray => {
5590                    if self.graph.is_dirty(v) || self.graph.is_volatile(v) {
5591                        to_evaluate.insert(v);
5592                    }
5593                }
5594                _ => {}
5595            }
5596
5597            // Explicit dependencies (graph edges)
5598            if let Some(dependencies) = self.graph.dependencies_slice(v) {
5599                for &dep in dependencies {
5600                    if self.graph.vertex_exists(dep) {
5601                        match self.graph.get_vertex_kind(dep) {
5602                            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
5603                                if !visited.contains(&dep) {
5604                                    stack.push(dep);
5605                                }
5606                            }
5607                            _ => {}
5608                        }
5609                    }
5610                }
5611            } else {
5612                for dep in self.graph.get_dependencies(v) {
5613                    if self.graph.vertex_exists(dep) {
5614                        match self.graph.get_vertex_kind(dep) {
5615                            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
5616                                if !visited.contains(&dep) {
5617                                    stack.push(dep);
5618                                }
5619                            }
5620                            _ => {}
5621                        }
5622                    }
5623                }
5624            } // Virtual dependencies (compressed ranges + dynamic like INDIRECT)
5625            let builder = VirtualDepBuilder::new(self);
5626            let (vdeps_map, _) = builder.build(&[v]);
5627            if let Some(deps) = vdeps_map.get(&v) {
5628                for &u in deps {
5629                    vdeps.entry(v).or_default().push(u);
5630                    if !visited.contains(&u) {
5631                        stack.push(u);
5632                    }
5633                }
5634            }
5635        }
5636
5637        let mut result: Vec<VertexId> = to_evaluate.into_iter().collect();
5638        result.sort_unstable();
5639        // Dedup virtual deps
5640        for deps in vdeps.values_mut() {
5641            deps.sort_unstable();
5642            deps.dedup();
5643        }
5644        (result, vdeps)
5645    }
5646
5647    /// Helper: convert 1-based column index to Excel-style letters (1 -> A, 27 -> AA)
5648    fn col_to_letters(col: u32) -> String {
5649        col_letters_from_1based(col).expect("column index must be >= 1")
5650    }
5651
5652    /// Evaluate all dirty/volatile vertices with cancellation support
5653    pub fn evaluate_all_cancellable(
5654        &mut self,
5655        cancel_flag: Arc<AtomicBool>,
5656    ) -> Result<EvalResult, ExcelError> {
5657        self.active_cancel_flag = Some(cancel_flag.clone());
5658        let res = self.evaluate_all_cancellable_impl(&cancel_flag);
5659        self.active_cancel_flag = None;
5660        res
5661    }
5662
5663    fn evaluate_all_cancellable_impl(
5664        &mut self,
5665        cancel_flag: &AtomicBool,
5666    ) -> Result<EvalResult, ExcelError> {
5667        let _source_cache = self.source_cache_session();
5668        self.validate_deterministic_mode()?;
5669        if self.config.defer_graph_building {
5670            self.build_graph_all()?;
5671        }
5672        self.reset_virtual_dep_telemetry_if_disabled();
5673        let start = crate::instant::FzInstant::now();
5674        let mut computed_vertices = 0;
5675        let mut cycle_errors = 0;
5676
5677        let mut replan_iterations = 0;
5678        const MAX_REPLAN: usize = 5;
5679        let mut telemetry = self
5680            .config
5681            .enable_virtual_dep_telemetry
5682            .then(|| self.start_virtual_dep_telemetry());
5683
5684        loop {
5685            if cancel_flag.load(Ordering::Relaxed) {
5686                if let Some(mut t) = telemetry {
5687                    t.bailout_reason = Some("cancelled");
5688                    t.replan_iterations = replan_iterations;
5689                    self.last_virtual_dep_telemetry = t;
5690                }
5691                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
5692                    .with_message("Evaluation cancelled before scheduling".to_string()));
5693            }
5694
5695            let to_evaluate = self.graph.get_evaluation_vertices();
5696            if to_evaluate.is_empty() {
5697                if let Some(t) = telemetry.as_mut()
5698                    && t.bailout_reason.is_none()
5699                {
5700                    t.bailout_reason = Some("no_work");
5701                }
5702                break;
5703            }
5704
5705            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
5706            if let Some(t) = telemetry.as_mut() {
5707                Self::accumulate_schedule_meta(t, &meta);
5708            }
5709
5710            // Handle cycles first by marking them with #CIRC!
5711            for cycle in &schedule.cycles {
5712                // Check cancellation between cycles
5713                if cancel_flag.load(Ordering::Relaxed) {
5714                    if let Some(mut t) = telemetry {
5715                        t.bailout_reason = Some("cancelled");
5716                        t.replan_iterations = replan_iterations;
5717                        self.last_virtual_dep_telemetry = t;
5718                    }
5719                    return Err(ExcelError::new(ExcelErrorKind::Cancelled)
5720                        .with_message("Evaluation cancelled during cycle handling".to_string()));
5721                }
5722
5723                cycle_errors += 1;
5724                let circ_error = LiteralValue::Error(
5725                    ExcelError::new(ExcelErrorKind::Circ)
5726                        .with_message("Circular dependency detected".to_string()),
5727                );
5728                for &vertex_id in cycle {
5729                    self.graph
5730                        .update_vertex_value(vertex_id, circ_error.clone());
5731                    self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
5732                }
5733            }
5734
5735            // Evaluate acyclic layers sequentially with cancellation checks
5736            for layer in &schedule.layers {
5737                // Check cancellation between layers
5738                if cancel_flag.load(Ordering::Relaxed) {
5739                    if let Some(mut t) = telemetry {
5740                        t.bailout_reason = Some("cancelled");
5741                        t.replan_iterations = replan_iterations;
5742                        self.last_virtual_dep_telemetry = t;
5743                    }
5744                    return Err(ExcelError::new(ExcelErrorKind::Cancelled)
5745                        .with_message("Evaluation cancelled between layers".to_string()));
5746                }
5747
5748                // Evaluate vertices in this layer (parallel or sequential)
5749                if self.thread_pool.is_some() && layer.vertices.len() > 1 {
5750                    computed_vertices +=
5751                        self.evaluate_layer_parallel_cancellable(layer, cancel_flag)?;
5752                } else {
5753                    computed_vertices +=
5754                        self.evaluate_layer_sequential_cancellable(layer, cancel_flag)?;
5755                }
5756            }
5757
5758            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
5759            if let Some(t) = telemetry.as_mut() {
5760                t.changed_vdeps_total += changed_vertices.len();
5761            }
5762            self.graph.clear_dirty_flags(&to_evaluate);
5763            for v in &changed_vertices {
5764                self.graph.set_dirty(*v, true);
5765            }
5766
5767            if changed_vertices.is_empty() {
5768                if let Some(t) = telemetry.as_mut() {
5769                    t.bailout_reason = Some("converged");
5770                }
5771                break;
5772            }
5773            if replan_iterations >= MAX_REPLAN {
5774                if let Some(t) = telemetry.as_mut() {
5775                    t.bailout_reason = Some("max_replan");
5776                }
5777                break;
5778            }
5779            replan_iterations += 1;
5780        }
5781
5782        if let Some(mut t) = telemetry {
5783            t.replan_iterations = replan_iterations;
5784            self.last_virtual_dep_telemetry = t;
5785        }
5786
5787        // Re-dirty volatile vertices for the next evaluation cycle
5788        self.graph.redirty_volatiles();
5789        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
5790
5791        Ok(EvalResult {
5792            computed_vertices,
5793            cycle_errors,
5794            elapsed: start.elapsed(),
5795        })
5796    }
5797
5798    /// Evaluate only the necessary precedents for specific target cells with cancellation support
5799    pub fn evaluate_until_cancellable(
5800        &mut self,
5801        targets: &[&str],
5802        cancel_flag: Arc<AtomicBool>,
5803    ) -> Result<EvalResult, ExcelError> {
5804        self.active_cancel_flag = Some(cancel_flag.clone());
5805        let res = self.evaluate_until_cancellable_impl(targets, &cancel_flag);
5806        self.active_cancel_flag = None;
5807        res
5808    }
5809
5810    fn evaluate_until_cancellable_impl(
5811        &mut self,
5812        targets: &[&str],
5813        cancel_flag: &AtomicBool,
5814    ) -> Result<EvalResult, ExcelError> {
5815        let start = crate::instant::FzInstant::now();
5816
5817        // Parse target cell addresses
5818        let mut target_addrs = Vec::new();
5819        for target in targets {
5820            let (sheet, row, col) = self.parse_a1_notation(target)?;
5821            let sheet_id = self.graph.sheet_id_mut(&sheet);
5822            let coord = Coord::from_excel(row, col, true, true);
5823            target_addrs.push(CellRef::new(sheet_id, coord));
5824        }
5825
5826        // Find vertex IDs for targets
5827        let mut target_vertex_ids = Vec::new();
5828        for addr in &target_addrs {
5829            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
5830                target_vertex_ids.push(*vertex_id);
5831            }
5832        }
5833
5834        if target_vertex_ids.is_empty() {
5835            return Ok(EvalResult {
5836                computed_vertices: 0,
5837                cycle_errors: 0,
5838                elapsed: start.elapsed(),
5839            });
5840        }
5841
5842        // Build demand subgraph with virtual edges
5843        let (precedents_to_eval, vdeps) = self.build_demand_subgraph(&target_vertex_ids);
5844
5845        if precedents_to_eval.is_empty() {
5846            return Ok(EvalResult {
5847                computed_vertices: 0,
5848                cycle_errors: 0,
5849                elapsed: start.elapsed(),
5850            });
5851        }
5852
5853        // Create schedule honoring virtual edges
5854        let scheduler = Scheduler::new(&self.graph);
5855        let schedule = scheduler.create_schedule_with_virtual(&precedents_to_eval, &vdeps)?;
5856
5857        // Handle cycles first
5858        let mut cycle_errors = 0;
5859        for cycle in &schedule.cycles {
5860            // Check cancellation between cycles
5861            if cancel_flag.load(Ordering::Relaxed) {
5862                return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
5863                    "Demand-driven evaluation cancelled during cycle handling".to_string(),
5864                ));
5865            }
5866
5867            cycle_errors += 1;
5868            let circ_error = LiteralValue::Error(
5869                ExcelError::new(ExcelErrorKind::Circ)
5870                    .with_message("Circular dependency detected".to_string()),
5871            );
5872            for &vertex_id in cycle {
5873                self.graph
5874                    .update_vertex_value(vertex_id, circ_error.clone());
5875                self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
5876            }
5877        }
5878
5879        // Evaluate layers with cancellation checks
5880        let mut computed_vertices = 0;
5881        for layer in &schedule.layers {
5882            // Check cancellation between layers
5883            if cancel_flag.load(Ordering::Relaxed) {
5884                return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
5885                    "Demand-driven evaluation cancelled between layers".to_string(),
5886                ));
5887            }
5888
5889            // Evaluate vertices in this layer (parallel or sequential)
5890            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
5891                computed_vertices +=
5892                    self.evaluate_layer_parallel_cancellable(layer, cancel_flag)?;
5893            } else {
5894                computed_vertices +=
5895                    self.evaluate_layer_sequential_cancellable_demand_driven(layer, cancel_flag)?;
5896            }
5897        }
5898
5899        // Clear dirty flags for evaluated vertices
5900        self.graph.clear_dirty_flags(&precedents_to_eval);
5901
5902        // Re-dirty volatile vertices
5903        self.graph.redirty_volatiles();
5904
5905        Ok(EvalResult {
5906            computed_vertices,
5907            cycle_errors,
5908            elapsed: start.elapsed(),
5909        })
5910    }
5911
5912    fn parse_a1_notation(&self, address: &str) -> Result<(String, u32, u32), ExcelError> {
5913        let mut parts = address.splitn(2, '!');
5914        let first = parts.next().unwrap_or_default();
5915        let remainder = parts.next();
5916
5917        let (sheet, cell_part) = match remainder {
5918            Some(cell) => (first.to_string(), cell),
5919            None => (self.default_sheet_name().to_string(), first),
5920        };
5921
5922        let (row, col, _, _) = parse_a1_1based(cell_part).map_err(|err| {
5923            ExcelError::new(ExcelErrorKind::Ref)
5924                .with_message(format!("Invalid cell reference `{cell_part}`: {err}"))
5925        })?;
5926
5927        Ok((sheet, row, col))
5928    }
5929
5930    /// Determine volatility using this engine's FunctionProvider, falling back to global registry.
5931    fn is_ast_volatile_with_provider(&self, ast: &ASTNode) -> bool {
5932        use formualizer_parse::parser::ASTNodeType;
5933        match &ast.node_type {
5934            ASTNodeType::Function { name, args, .. } => {
5935                if let Some(func) = self
5936                    .get_function("", name)
5937                    .or_else(|| crate::function_registry::get("", name))
5938                    && func.caps().contains(crate::function::FnCaps::VOLATILE)
5939                {
5940                    return true;
5941                }
5942                args.iter()
5943                    .any(|arg| self.is_ast_volatile_with_provider(arg))
5944            }
5945            ASTNodeType::BinaryOp { left, right, .. } => {
5946                self.is_ast_volatile_with_provider(left)
5947                    || self.is_ast_volatile_with_provider(right)
5948            }
5949            ASTNodeType::UnaryOp { expr, .. } => self.is_ast_volatile_with_provider(expr),
5950            ASTNodeType::Array(rows) => rows.iter().any(|row| {
5951                row.iter()
5952                    .any(|cell| self.is_ast_volatile_with_provider(cell))
5953            }),
5954            _ => false,
5955        }
5956    }
5957
5958    /// Find dirty precedents that need evaluation for the given target vertices
5959    fn find_dirty_precedents(&self, target_vertices: &[VertexId]) -> Vec<VertexId> {
5960        let mut to_evaluate = FxHashSet::default();
5961        let mut visited = FxHashSet::default();
5962        let mut stack = Vec::new();
5963
5964        // Start reverse traversal from target vertices
5965        for &target in target_vertices {
5966            stack.push(target);
5967        }
5968
5969        while let Some(vertex_id) = stack.pop() {
5970            if !visited.insert(vertex_id) {
5971                continue; // Already processed
5972            }
5973
5974            if self.graph.vertex_exists(vertex_id) {
5975                // Check if this vertex needs evaluation
5976                let kind = self.graph.get_vertex_kind(vertex_id);
5977                let needs_eval = match kind {
5978                    super::vertex::VertexKind::FormulaScalar
5979                    | super::vertex::VertexKind::FormulaArray => {
5980                        self.graph.is_dirty(vertex_id) || self.graph.is_volatile(vertex_id)
5981                    }
5982                    _ => false, // Values and empty cells don't need evaluation
5983                };
5984
5985                if needs_eval {
5986                    to_evaluate.insert(vertex_id);
5987                }
5988
5989                // Continue traversal to dependencies (precedents)
5990                if let Some(dependencies) = self.graph.dependencies_slice(vertex_id) {
5991                    for &dep_id in dependencies {
5992                        if !visited.contains(&dep_id) {
5993                            stack.push(dep_id);
5994                        }
5995                    }
5996                } else {
5997                    let dependencies = self.graph.get_dependencies(vertex_id);
5998                    for dep_id in dependencies {
5999                        if !visited.contains(&dep_id) {
6000                            stack.push(dep_id);
6001                        }
6002                    }
6003                }
6004            }
6005        }
6006
6007        let mut result: Vec<VertexId> = to_evaluate.into_iter().collect();
6008        result.sort_unstable();
6009        result
6010    }
6011
6012    /// Evaluate a layer sequentially
6013    fn evaluate_layer_sequential(
6014        &mut self,
6015        layer: &super::scheduler::Layer,
6016    ) -> Result<usize, ExcelError> {
6017        self.evaluate_layer_sequential_effects(layer)
6018    }
6019
6020    fn update_vertex_value_with_delta(
6021        &mut self,
6022        vertex_id: VertexId,
6023        new_value: LiteralValue,
6024        delta: &mut DeltaCollector,
6025    ) {
6026        if delta.mode != DeltaMode::Off
6027            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
6028        {
6029            let sheet_name = self.graph.sheet_name(cell.sheet_id);
6030            let old = self
6031                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
6032                .unwrap_or(LiteralValue::Empty);
6033            if old != new_value {
6034                delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
6035            }
6036        }
6037        self.graph.update_vertex_value(vertex_id, new_value.clone());
6038        self.mirror_vertex_value_to_overlay(vertex_id, &new_value);
6039    }
6040
6041    fn evaluate_layer_sequential_with_delta(
6042        &mut self,
6043        layer: &super::scheduler::Layer,
6044        delta: &mut DeltaCollector,
6045    ) -> Result<usize, ExcelError> {
6046        self.evaluate_layer_sequential_with_delta_effects(layer, delta)
6047    }
6048
6049    /// Evaluate a layer sequentially with cancellation support
6050    fn evaluate_layer_sequential_cancellable(
6051        &mut self,
6052        layer: &super::scheduler::Layer,
6053        cancel_flag: &AtomicBool,
6054    ) -> Result<usize, ExcelError> {
6055        self.evaluate_layer_sequential_cancellable_effects(layer, cancel_flag)
6056    }
6057
6058    /// Evaluate a layer sequentially with more frequent cancellation checks for demand-driven evaluation
6059    fn evaluate_layer_sequential_cancellable_demand_driven(
6060        &mut self,
6061        layer: &super::scheduler::Layer,
6062        cancel_flag: &AtomicBool,
6063    ) -> Result<usize, ExcelError> {
6064        self.evaluate_layer_sequential_cancellable_demand_driven_effects(layer, cancel_flag)
6065    }
6066
6067    /// Evaluate a layer in parallel using the thread pool
6068    fn evaluate_layer_parallel(
6069        &mut self,
6070        layer: &super::scheduler::Layer,
6071    ) -> Result<usize, ExcelError> {
6072        self.evaluate_layer_parallel_effects(layer)
6073    }
6074
6075    fn evaluate_layer_parallel_with_delta(
6076        &mut self,
6077        layer: &super::scheduler::Layer,
6078        delta: &mut DeltaCollector,
6079    ) -> Result<usize, ExcelError> {
6080        self.evaluate_layer_parallel_with_delta_effects(layer, delta)
6081    }
6082
6083    /// Evaluate a layer in parallel with cancellation support
6084    fn evaluate_layer_parallel_cancellable(
6085        &mut self,
6086        layer: &super::scheduler::Layer,
6087        cancel_flag: &AtomicBool,
6088    ) -> Result<usize, ExcelError> {
6089        self.evaluate_layer_parallel_cancellable_effects(layer, cancel_flag)
6090    }
6091
6092    /// Apply a computed result produced by `evaluate_vertex_immutable()`.
6093    ///
6094    /// This is the parallel equivalent of the "apply" portion of `evaluate_vertex_impl`.
6095    /// We keep apply sequential for correctness (spill commit is inherently stateful).
6096    fn apply_parallel_vertex_result(
6097        &mut self,
6098        vertex_id: VertexId,
6099        result: LiteralValue,
6100        mut delta: Option<&mut DeltaCollector>,
6101        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
6102    ) -> Result<(), ExcelError> {
6103        // If this vertex's cell is currently covered by a spill from a different anchor,
6104        // ignore the computed result. The spill's committed values own the grid.
6105        if let Some(cell) = self.graph.get_cell_ref(vertex_id)
6106            && let Some(owner) = self.graph.spill_registry_anchor_for_cell(cell)
6107            && owner != vertex_id
6108        {
6109            return Ok(());
6110        }
6111
6112        let kind = self.graph.get_vertex_kind(vertex_id);
6113
6114        // Only formula vertices spill dynamic arrays into the grid.
6115        let is_formula = matches!(kind, VertexKind::FormulaScalar | VertexKind::FormulaArray);
6116        if is_formula {
6117            match result {
6118                LiteralValue::Array(rows) => {
6119                    self.apply_array_result_from_parallel(
6120                        vertex_id,
6121                        rows,
6122                        delta.as_deref_mut(),
6123                        overwritable_formulas,
6124                    )?;
6125                }
6126                other => {
6127                    self.apply_non_array_result_from_parallel(
6128                        vertex_id,
6129                        other,
6130                        delta.as_deref_mut(),
6131                    );
6132                }
6133            }
6134            return Ok(());
6135        }
6136
6137        // Non-formula vertices: store value as-is (arrays remain arrays; no spill).
6138        if let Some(d) = delta {
6139            self.update_vertex_value_with_delta(vertex_id, result, d);
6140        } else {
6141            self.graph.update_vertex_value(vertex_id, result.clone());
6142            self.mirror_vertex_value_to_overlay(vertex_id, &result);
6143        }
6144        Ok(())
6145    }
6146
6147    fn apply_non_array_result_from_parallel(
6148        &mut self,
6149        vertex_id: VertexId,
6150        value: LiteralValue,
6151        delta: Option<&mut DeltaCollector>,
6152    ) {
6153        // Scalar/error result: store value and ensure any previous spill is cleared.
6154        // This mirrors the sequential behavior in `evaluate_vertex_impl`.
6155        let spill_cells = self
6156            .graph
6157            .spill_cells_for_anchor(vertex_id)
6158            .map(|cells| cells.to_vec())
6159            .unwrap_or_default();
6160
6161        if let Some(d) = delta
6162            && d.mode != DeltaMode::Off
6163            && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
6164        {
6165            if spill_cells.is_empty() {
6166                let old = self
6167                    .read_cell_value(
6168                        self.graph.sheet_name(anchor.sheet_id),
6169                        anchor.coord.row() + 1,
6170                        anchor.coord.col() + 1,
6171                    )
6172                    .unwrap_or(LiteralValue::Empty);
6173                if old != value {
6174                    d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
6175                }
6176            } else {
6177                for cell in spill_cells.iter() {
6178                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
6179                    let old = self
6180                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
6181                        .unwrap_or(LiteralValue::Empty);
6182                    let new = if cell.sheet_id == anchor.sheet_id
6183                        && cell.coord.row() == anchor.coord.row()
6184                        && cell.coord.col() == anchor.coord.col()
6185                    {
6186                        value.clone()
6187                    } else {
6188                        LiteralValue::Empty
6189                    };
6190                    Self::record_cell_if_changed(d, cell, &old, &new);
6191                }
6192            }
6193        }
6194
6195        self.graph.clear_spill_region(vertex_id);
6196
6197        if self.config.arrow_storage_enabled
6198            && self.config.delta_overlay_enabled
6199            && self.config.write_formula_overlay_enabled
6200        {
6201            let empty = LiteralValue::Empty;
6202            for cell in spill_cells.iter() {
6203                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
6204                self.mirror_value_to_computed_overlay(
6205                    &sheet_name,
6206                    cell.coord.row() + 1,
6207                    cell.coord.col() + 1,
6208                    &empty,
6209                );
6210            }
6211        }
6212
6213        self.graph.update_vertex_value(vertex_id, value.clone());
6214        self.mirror_vertex_value_to_overlay(vertex_id, &value);
6215    }
6216
6217    fn apply_array_result_from_parallel(
6218        &mut self,
6219        vertex_id: VertexId,
6220        rows: Vec<Vec<LiteralValue>>,
6221        mut delta: Option<&mut DeltaCollector>,
6222        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
6223    ) -> Result<(), ExcelError> {
6224        // Keep behavior consistent with the sequential spill path in `evaluate_vertex_impl`.
6225        self.graph
6226            .set_kind(vertex_id, crate::engine::vertex::VertexKind::FormulaArray);
6227
6228        let anchor = self
6229            .graph
6230            .get_cell_ref(vertex_id)
6231            .expect("cell ref for vertex");
6232        let sheet_id = anchor.sheet_id;
6233        let h = rows.len() as u32;
6234        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
6235
6236        // Hard cap to avoid vertex explosion from huge dynamic arrays.
6237        let spill_cells = (h as u64).saturating_mul(w as u64);
6238        if spill_cells > self.config.spill.max_spill_cells as u64 {
6239            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
6240            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
6241                .with_message("SpillTooLarge")
6242                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
6243                    expected_rows: h,
6244                    expected_cols: w,
6245                });
6246            let spill_val = LiteralValue::Error(spill_err.clone());
6247            if let Some(d) = delta.as_deref_mut()
6248                && d.mode != DeltaMode::Off
6249            {
6250                let old = self
6251                    .read_cell_value(
6252                        self.graph.sheet_name(anchor.sheet_id),
6253                        anchor.coord.row() + 1,
6254                        anchor.coord.col() + 1,
6255                    )
6256                    .unwrap_or(LiteralValue::Empty);
6257                if old != spill_val {
6258                    d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
6259                }
6260            }
6261            self.graph.update_vertex_value(vertex_id, spill_val.clone());
6262            self.mirror_vertex_value_to_overlay(vertex_id, &spill_val);
6263            return Ok(());
6264        }
6265
6266        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity)
6267        const PACKED_MAX_ROW: u32 = 1_048_575; // 20-bit max
6268        const PACKED_MAX_COL: u32 = 16_383; // 14-bit max
6269        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
6270        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
6271        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
6272            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
6273            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
6274                .with_message("Spill exceeds sheet bounds")
6275                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
6276                    expected_rows: h,
6277                    expected_cols: w,
6278                });
6279            let spill_val = LiteralValue::Error(spill_err.clone());
6280            if let Some(d) = delta.as_deref_mut()
6281                && d.mode != DeltaMode::Off
6282            {
6283                let old = self
6284                    .read_cell_value(
6285                        self.graph.sheet_name(anchor.sheet_id),
6286                        anchor.coord.row() + 1,
6287                        anchor.coord.col() + 1,
6288                    )
6289                    .unwrap_or(LiteralValue::Empty);
6290                if old != spill_val {
6291                    d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
6292                }
6293            }
6294            self.graph.update_vertex_value(vertex_id, spill_val.clone());
6295            self.mirror_vertex_value_to_overlay(vertex_id, &spill_val);
6296            return Ok(());
6297        }
6298
6299        let mut targets = Vec::new();
6300        for r in 0..h {
6301            for c in 0..w {
6302                targets.push(self.graph.make_cell_ref_internal(
6303                    sheet_id,
6304                    anchor.coord.row() + r,
6305                    anchor.coord.col() + c,
6306                ));
6307            }
6308        }
6309
6310        match self.spill_mgr.reserve(
6311            vertex_id,
6312            anchor,
6313            SpillShape { rows: h, cols: w },
6314            SpillMeta {
6315                epoch: self.recalc_epoch,
6316                config: self.config.spill,
6317            },
6318        ) {
6319            Ok(()) => {
6320                if let Err(e) = self.commit_spill_and_mirror(
6321                    vertex_id,
6322                    &targets,
6323                    rows.clone(),
6324                    delta.as_deref_mut(),
6325                    overwritable_formulas,
6326                ) {
6327                    self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
6328                    let err_val = LiteralValue::Error(e.clone());
6329                    if let Some(d) = delta.as_deref_mut()
6330                        && d.mode != DeltaMode::Off
6331                    {
6332                        let old = self
6333                            .read_cell_value(
6334                                self.graph.sheet_name(anchor.sheet_id),
6335                                anchor.coord.row() + 1,
6336                                anchor.coord.col() + 1,
6337                            )
6338                            .unwrap_or(LiteralValue::Empty);
6339                        if old != err_val {
6340                            d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
6341                        }
6342                    }
6343                    self.graph.update_vertex_value(vertex_id, err_val.clone());
6344                    self.mirror_vertex_value_to_overlay(vertex_id, &err_val);
6345                    return Ok(());
6346                }
6347
6348                // Anchor shows the top-left value, like Excel
6349                let top_left = rows
6350                    .first()
6351                    .and_then(|r| r.first())
6352                    .cloned()
6353                    .unwrap_or(LiteralValue::Empty);
6354                self.graph.update_vertex_value(vertex_id, top_left.clone());
6355                self.mirror_vertex_value_to_overlay(vertex_id, &top_left);
6356                Ok(())
6357            }
6358            Err(e) => {
6359                self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
6360                let spill_err = ExcelError::new(ExcelErrorKind::Spill)
6361                    .with_message(e.message.unwrap_or_else(|| "Spill blocked".to_string()))
6362                    .with_extra(formualizer_common::ExcelErrorExtra::Spill {
6363                        expected_rows: h,
6364                        expected_cols: w,
6365                    });
6366                let spill_val = LiteralValue::Error(spill_err.clone());
6367                if let Some(d) = delta
6368                    && d.mode != DeltaMode::Off
6369                {
6370                    let old = self
6371                        .read_cell_value(
6372                            self.graph.sheet_name(anchor.sheet_id),
6373                            anchor.coord.row() + 1,
6374                            anchor.coord.col() + 1,
6375                        )
6376                        .unwrap_or(LiteralValue::Empty);
6377                    if old != spill_val {
6378                        d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
6379                    }
6380                }
6381                self.graph.update_vertex_value(vertex_id, spill_val.clone());
6382                self.mirror_vertex_value_to_overlay(vertex_id, &spill_val);
6383                Ok(())
6384            }
6385        }
6386    }
6387
6388    /// Evaluate a single vertex without mutating the graph (for parallel evaluation)
6389    fn evaluate_vertex_immutable(&self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
6390        // Check if vertex exists
6391        if !self.graph.vertex_exists(vertex_id) {
6392            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
6393                .with_message(format!("Vertex not found: {vertex_id:?}")));
6394        }
6395
6396        // Get vertex kind and check if it needs evaluation
6397        let kind = self.graph.get_vertex_kind(vertex_id);
6398        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
6399
6400        let ast_id = match kind {
6401            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
6402                if let Some(ast_id) = self.graph.get_formula_id(vertex_id) {
6403                    ast_id
6404                } else {
6405                    return Ok(LiteralValue::Number(0.0));
6406                }
6407            }
6408            VertexKind::Empty | VertexKind::Cell => {
6409                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
6410                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
6411                    let row = cell_ref.coord.row() + 1;
6412                    let col = cell_ref.coord.col() + 1;
6413                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
6414                        return Ok(v);
6415                    }
6416                }
6417                return Ok(LiteralValue::Number(0.0));
6418            }
6419            VertexKind::NamedScalar => {
6420                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
6421                    ExcelError::new(ExcelErrorKind::Name)
6422                        .with_message("Named range metadata missing".to_string())
6423                })?;
6424
6425                return match &named_range.definition {
6426                    NamedDefinition::Cell(cell_ref) => {
6427                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
6428                        Ok(self
6429                            .get_cell_value(
6430                                sheet_name,
6431                                cell_ref.coord.row() + 1,
6432                                cell_ref.coord.col() + 1,
6433                            )
6434                            .unwrap_or(LiteralValue::Empty))
6435                    }
6436                    NamedDefinition::Literal(v) => Ok(v.clone()),
6437                    NamedDefinition::Formula { ast, .. } => {
6438                        let context_sheet = match named_range.scope {
6439                            NameScope::Sheet(id) => id,
6440                            NameScope::Workbook => sheet_id,
6441                        };
6442                        let sheet_name = self.graph.sheet_name(context_sheet);
6443                        let cell_ref = self
6444                            .graph
6445                            .get_cell_ref(vertex_id)
6446                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
6447                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
6448                        interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
6449                    }
6450                    NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
6451                        .with_message("Range-valued name evaluated as scalar".to_string())),
6452                };
6453            }
6454            VertexKind::NamedArray => {
6455                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
6456                    ExcelError::new(ExcelErrorKind::Name)
6457                        .with_message("Named range metadata missing".to_string())
6458                })?;
6459
6460                return match &named_range.definition {
6461                    NamedDefinition::Range(range_ref) => {
6462                        if range_ref.start.sheet_id != range_ref.end.sheet_id {
6463                            return Err(ExcelError::new(ExcelErrorKind::Ref)
6464                                .with_message("Named range cannot span sheets".to_string()));
6465                        }
6466                        let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
6467                        let sr0 = range_ref.start.coord.row();
6468                        let sc0 = range_ref.start.coord.col();
6469                        let er0 = range_ref.end.coord.row();
6470                        let ec0 = range_ref.end.coord.col();
6471                        if sr0 > er0 || sc0 > ec0 {
6472                            return Err(ExcelError::new(ExcelErrorKind::Ref)
6473                                .with_message("Invalid named range bounds".to_string()));
6474                        }
6475
6476                        let h = (er0 - sr0 + 1) as usize;
6477                        let w = (ec0 - sc0 + 1) as usize;
6478                        let cell_count = (h as u64).saturating_mul(w as u64);
6479                        if cell_count > self.config.spill.max_spill_cells as u64 {
6480                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
6481                                "Named range too large to materialize as an array".to_string(),
6482                            ));
6483                        }
6484
6485                        let mut rows = Vec::with_capacity(h);
6486                        for r0 in sr0..=er0 {
6487                            let mut row = Vec::with_capacity(w);
6488                            for c0 in sc0..=ec0 {
6489                                let v = self
6490                                    .get_cell_value(sheet_name, r0 + 1, c0 + 1)
6491                                    .unwrap_or(LiteralValue::Empty);
6492                                row.push(v);
6493                            }
6494                            rows.push(row);
6495                        }
6496                        Ok(LiteralValue::Array(rows))
6497                    }
6498                    NamedDefinition::Cell(cell_ref) => {
6499                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
6500                        let row = cell_ref.coord.row() + 1;
6501                        let col = cell_ref.coord.col() + 1;
6502                        let v = self
6503                            .get_cell_value(sheet_name, row, col)
6504                            .unwrap_or(LiteralValue::Empty);
6505                        Ok(LiteralValue::Array(vec![vec![v]]))
6506                    }
6507                    NamedDefinition::Literal(v) => Ok(LiteralValue::Array(vec![vec![v.clone()]])),
6508                    NamedDefinition::Formula { ast, .. } => {
6509                        let context_sheet = match named_range.scope {
6510                            NameScope::Sheet(id) => id,
6511                            NameScope::Workbook => sheet_id,
6512                        };
6513                        let sheet_name = self.graph.sheet_name(context_sheet);
6514                        let cell_ref = self
6515                            .graph
6516                            .get_cell_ref(vertex_id)
6517                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
6518                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
6519                        match interpreter.evaluate_ast(ast) {
6520                            Ok(cv) => {
6521                                let v = cv.into_literal();
6522                                match v {
6523                                    LiteralValue::Array(_) => Ok(v),
6524                                    other => Ok(LiteralValue::Array(vec![vec![other]])),
6525                                }
6526                            }
6527                            Err(err) => Ok(LiteralValue::Error(err)),
6528                        }
6529                    }
6530                };
6531            }
6532            VertexKind::InfiniteRange
6533            | VertexKind::Range
6534            | VertexKind::External
6535            | VertexKind::Table => {
6536                // Not directly evaluatable here.
6537                return Ok(LiteralValue::Number(0.0));
6538            }
6539        };
6540
6541        // The interpreter uses a reference to the engine as the context
6542        let sheet_name = self.graph.sheet_name(sheet_id);
6543        let cell_ref = self
6544            .graph
6545            .get_cell_ref(vertex_id)
6546            .expect("cell ref for vertex");
6547        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
6548
6549        interpreter
6550            .evaluate_arena_ast(ast_id, self.graph.data_store(), self.graph.sheet_reg())
6551            .map(|cv| cv.into_literal())
6552    }
6553
6554    /// Get access to the shared thread pool for parallel evaluation
6555    pub fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
6556        self.thread_pool.as_ref()
6557    }
6558}
6559
6560#[derive(Default)]
6561struct RowBoundsCache {
6562    snapshot: u64,
6563    // key: (sheet_id, col_idx)
6564    map: rustc_hash::FxHashMap<(u32, usize), (Option<u32>, Option<u32>)>,
6565}
6566
6567impl RowBoundsCache {
6568    fn new(snapshot: u64) -> Self {
6569        Self {
6570            snapshot,
6571            map: Default::default(),
6572        }
6573    }
6574    fn get_row_bounds(
6575        &self,
6576        sheet_id: SheetId,
6577        col_idx: usize,
6578        snapshot: u64,
6579    ) -> Option<(Option<u32>, Option<u32>)> {
6580        if self.snapshot != snapshot {
6581            return None;
6582        }
6583        self.map.get(&(sheet_id as u32, col_idx)).copied()
6584    }
6585    fn put_row_bounds(
6586        &mut self,
6587        sheet_id: SheetId,
6588        col_idx: usize,
6589        snapshot: u64,
6590        bounds: (Option<u32>, Option<u32>),
6591    ) {
6592        if self.snapshot != snapshot {
6593            self.snapshot = snapshot;
6594            self.map.clear();
6595        }
6596        self.map.insert((sheet_id as u32, col_idx), bounds);
6597    }
6598}
6599
6600// Phase 2 shim: in-process spill manager delegating to current graph methods.
6601#[derive(Default)]
6602pub struct ShimSpillManager {
6603    region_locks: RegionLockManager,
6604    pub(crate) active_locks: rustc_hash::FxHashMap<VertexId, u64>,
6605}
6606
6607impl ShimSpillManager {
6608    pub(crate) fn reserve(
6609        &mut self,
6610        owner: VertexId,
6611        anchor_cell: CellRef,
6612        shape: SpillShape,
6613        _meta: SpillMeta,
6614    ) -> Result<(), ExcelError> {
6615        // Derive region from anchor + shape; enforce in-flight exclusivity only.
6616        let region = crate::engine::spill::Region {
6617            sheet_id: anchor_cell.sheet_id as u32,
6618            row_start: anchor_cell.coord.row(),
6619            row_end: anchor_cell
6620                .coord
6621                .row()
6622                .saturating_add(shape.rows)
6623                .saturating_sub(1),
6624            col_start: anchor_cell.coord.col(),
6625            col_end: anchor_cell
6626                .coord
6627                .col()
6628                .saturating_add(shape.cols)
6629                .saturating_sub(1),
6630        };
6631        match self.region_locks.reserve(region, owner) {
6632            Ok(id) => {
6633                if id != 0 {
6634                    self.active_locks.insert(owner, id);
6635                }
6636                Ok(())
6637            }
6638            Err(e) => Err(e),
6639        }
6640    }
6641
6642    pub(crate) fn commit_array_with_value_probe<F>(
6643        &mut self,
6644        graph: &mut DependencyGraph,
6645        anchor_vertex: VertexId,
6646        targets: &[CellRef],
6647        rows: Vec<Vec<LiteralValue>>,
6648        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
6649        mut value_probe: F,
6650    ) -> Result<(), ExcelError>
6651    where
6652        F: FnMut(&DependencyGraph, &CellRef) -> Option<LiteralValue>,
6653    {
6654        use formualizer_common::{ExcelErrorExtra, ExcelErrorKind};
6655
6656        // Re-run plan on concrete targets before committing to respect blockers.
6657        // This plan checks formula/spill ownership in the graph, but when the graph value cache
6658        // is disabled (Arrow-canonical mode), it cannot see non-empty value blockers.
6659        let plan_res = graph.plan_spill_region_allowing_formula_overwrite(
6660            anchor_vertex,
6661            targets,
6662            overwritable_formulas,
6663        );
6664        if let Err(e) = plan_res {
6665            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
6666                self.region_locks.release(id);
6667            }
6668            return Err(e);
6669        }
6670
6671        if !graph.value_cache_enabled() {
6672            // Compute expected spill shape from the target rectangle for diagnostics.
6673            let (expected_rows, expected_cols) = if targets.is_empty() {
6674                (0u32, 0u32)
6675            } else {
6676                let mut min_r = u32::MAX;
6677                let mut max_r = 0u32;
6678                let mut min_c = u32::MAX;
6679                let mut max_c = 0u32;
6680                for cell in targets {
6681                    let r = cell.coord.row();
6682                    let c = cell.coord.col();
6683                    min_r = min_r.min(r);
6684                    max_r = max_r.max(r);
6685                    min_c = min_c.min(c);
6686                    max_c = max_c.max(c);
6687                }
6688                (
6689                    max_r.saturating_sub(min_r).saturating_add(1),
6690                    max_c.saturating_sub(min_c).saturating_add(1),
6691                )
6692            };
6693
6694            let anchor_cell = graph
6695                .get_cell_ref(anchor_vertex)
6696                .expect("anchor cell ref for spill commit");
6697
6698            for cell in targets {
6699                // Never treat the anchor as a blocker.
6700                if *cell == anchor_cell {
6701                    continue;
6702                }
6703                // Skip cells already known to be owned by a spill; plan() handled spill conflicts.
6704                if graph.spill_registry_anchor_for_cell(*cell).is_some() {
6705                    continue;
6706                }
6707                // Skip formula vertices in the target region; plan() handled them (or allowed).
6708                if let Some(&vid) = graph.get_vertex_id_for_address(cell)
6709                    && vid != anchor_vertex
6710                {
6711                    match graph.get_vertex_kind(vid) {
6712                        crate::engine::vertex::VertexKind::FormulaScalar
6713                        | crate::engine::vertex::VertexKind::FormulaArray => {
6714                            // plan() already approved allowed overwrites.
6715                            continue;
6716                        }
6717                        _ => {}
6718                    }
6719                }
6720
6721                if let Some(v) = value_probe(graph, cell)
6722                    && !matches!(v, LiteralValue::Empty)
6723                {
6724                    if let Some(id) = self.active_locks.remove(&anchor_vertex) {
6725                        self.region_locks.release(id);
6726                    }
6727                    return Err(ExcelError::new(ExcelErrorKind::Spill)
6728                        .with_message("BlockedByValue")
6729                        .with_extra(ExcelErrorExtra::Spill {
6730                            expected_rows,
6731                            expected_cols,
6732                        }));
6733                }
6734            }
6735        }
6736
6737        let commit_res = graph.commit_spill_region_atomic_with_fault(
6738            anchor_vertex,
6739            targets.to_vec(),
6740            rows,
6741            None,
6742        );
6743        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
6744            self.region_locks.release(id);
6745        }
6746        commit_res.map(|_| ())
6747    }
6748
6749    /// Commit a spill and mirror all written cells into Arrow overlay via the owning engine.
6750    pub(crate) fn commit_array_with_overlay<R: EvaluationContext>(
6751        &mut self,
6752        engine: &mut Engine<R>,
6753        anchor_vertex: VertexId,
6754        targets: &[CellRef],
6755        rows: Vec<Vec<LiteralValue>>,
6756        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
6757    ) -> Result<(), ExcelError> {
6758        // Re-run plan on concrete targets before committing to respect blockers.
6759        let plan_res = engine.graph.plan_spill_region_allowing_formula_overwrite(
6760            anchor_vertex,
6761            targets,
6762            overwritable_formulas,
6763        );
6764        if let Err(e) = plan_res {
6765            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
6766                self.region_locks.release(id);
6767            }
6768            return Err(e);
6769        }
6770
6771        let commit_res = engine.graph.commit_spill_region_atomic_with_fault(
6772            anchor_vertex,
6773            targets.to_vec(),
6774            rows.clone(),
6775            None,
6776        );
6777        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
6778            self.region_locks.release(id);
6779        }
6780        commit_res.map(|_| ())?;
6781
6782        // Mirror into Arrow overlay when enabled
6783        if engine.config.arrow_storage_enabled
6784            && engine.config.delta_overlay_enabled
6785            && engine.config.write_formula_overlay_enabled
6786        {
6787            // Expect targets to be a contiguous rectangle row-major starting at some anchor
6788            for (idx, cell) in targets.iter().enumerate() {
6789                let (r_off, c_off) = {
6790                    if rows.is_empty() || rows[0].is_empty() {
6791                        (0usize, 0usize)
6792                    } else {
6793                        let width = rows[0].len();
6794                        (idx / width, idx % width)
6795                    }
6796                };
6797                let v = rows
6798                    .get(r_off)
6799                    .and_then(|r| r.get(c_off))
6800                    .cloned()
6801                    .unwrap_or(LiteralValue::Empty);
6802                let sheet_name = engine.graph.sheet_name(cell.sheet_id).to_string();
6803                engine.mirror_value_to_computed_overlay(
6804                    &sheet_name,
6805                    cell.coord.row() + 1,
6806                    cell.coord.col() + 1,
6807                    &v,
6808                );
6809            }
6810        }
6811        Ok(())
6812    }
6813}
6814
6815impl<R> Engine<R>
6816where
6817    R: EvaluationContext,
6818{
6819    fn resolve_shared_ref(
6820        &self,
6821        reference: &ReferenceType,
6822        current_sheet: &str,
6823    ) -> Result<formualizer_common::SheetRef<'static>, ExcelError> {
6824        use formualizer_common::{
6825            SheetCellRef as SharedCellRef, SheetLocator, SheetRangeRef as SharedRangeRef,
6826            SheetRef as SharedRef,
6827        };
6828
6829        // Preserve anchor flags from the parsed reference when possible.
6830        let sr = match reference {
6831            ReferenceType::Cell {
6832                sheet,
6833                row,
6834                col,
6835                row_abs,
6836                col_abs,
6837            } => {
6838                let row0 = row
6839                    .checked_sub(1)
6840                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
6841                let col0 = col
6842                    .checked_sub(1)
6843                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
6844                let sheet_loc = match sheet.as_deref() {
6845                    Some(name) => SheetLocator::from_name(name),
6846                    None => SheetLocator::Current,
6847                };
6848                let coord = formualizer_common::RelativeCoord::new(row0, col0, *row_abs, *col_abs);
6849                SharedRef::Cell(SharedCellRef::new(sheet_loc, coord))
6850            }
6851            ReferenceType::Range {
6852                sheet,
6853                start_row,
6854                start_col,
6855                end_row,
6856                end_col,
6857                start_row_abs,
6858                start_col_abs,
6859                end_row_abs,
6860                end_col_abs,
6861            } => {
6862                let sheet_loc = match sheet.as_deref() {
6863                    Some(name) => SheetLocator::from_name(name),
6864                    None => SheetLocator::Current,
6865                };
6866                let sr = start_row
6867                    .map(|r| {
6868                        r.checked_sub(1)
6869                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
6870                    })
6871                    .transpose()?;
6872                let sc = start_col
6873                    .map(|c| {
6874                        c.checked_sub(1)
6875                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
6876                    })
6877                    .transpose()?;
6878                let er = end_row
6879                    .map(|r| {
6880                        r.checked_sub(1)
6881                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
6882                    })
6883                    .transpose()?;
6884                let ec = end_col
6885                    .map(|c| {
6886                        c.checked_sub(1)
6887                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
6888                    })
6889                    .transpose()?;
6890                let range = SharedRangeRef::from_parts(
6891                    sheet_loc,
6892                    sr.map(|idx| formualizer_common::AxisBound::new(idx, *start_row_abs)),
6893                    sc.map(|idx| formualizer_common::AxisBound::new(idx, *start_col_abs)),
6894                    er.map(|idx| formualizer_common::AxisBound::new(idx, *end_row_abs)),
6895                    ec.map(|idx| formualizer_common::AxisBound::new(idx, *end_col_abs)),
6896                )
6897                .map_err(|_| ExcelError::new(ExcelErrorKind::Ref))?;
6898                SharedRef::Range(range)
6899            }
6900            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
6901        };
6902
6903        let current_id = self
6904            .graph
6905            .sheet_id(current_sheet)
6906            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
6907
6908        let resolve_loc = |loc: SheetLocator<'_>| -> Result<SheetLocator<'static>, ExcelError> {
6909            match loc {
6910                SheetLocator::Current => Ok(SheetLocator::Id(current_id)),
6911                SheetLocator::Id(id) => Ok(SheetLocator::Id(id)),
6912                SheetLocator::Name(name) => {
6913                    let n = name.as_ref();
6914                    self.graph
6915                        .sheet_id(n)
6916                        .map(SheetLocator::Id)
6917                        .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
6918                }
6919            }
6920        };
6921
6922        match sr {
6923            SharedRef::Cell(cell) => {
6924                let owned = cell.into_owned();
6925                let sheet = resolve_loc(owned.sheet)?;
6926                Ok(SharedRef::Cell(SharedCellRef::new(sheet, owned.coord)))
6927            }
6928            SharedRef::Range(range) => {
6929                let owned = range.into_owned();
6930                let sheet = resolve_loc(owned.sheet)?;
6931                Ok(SharedRef::Range(SharedRangeRef {
6932                    sheet,
6933                    start_row: owned.start_row,
6934                    start_col: owned.start_col,
6935                    end_row: owned.end_row,
6936                    end_col: owned.end_col,
6937                }))
6938            }
6939        }
6940    }
6941}
6942
6943// Implement the resolver traits for the Engine.
6944// This allows the interpreter to resolve references by querying the engine's graph.
6945impl<R> crate::traits::ReferenceResolver for Engine<R>
6946where
6947    R: EvaluationContext,
6948{
6949    fn resolve_cell_reference(
6950        &self,
6951        sheet: Option<&str>,
6952        row: u32,
6953        col: u32,
6954    ) -> Result<LiteralValue, ExcelError> {
6955        let sheet_name = sheet.unwrap_or_else(|| self.default_sheet_name()); // FIXME: should use formula current-sheet context
6956        // Prefer engine's unified accessor which consults Arrow store for base values
6957        // and falls back to graph for formulas and stored values.
6958        if let Some(v) = self.get_cell_value(sheet_name, row, col) {
6959            Ok(v)
6960        } else {
6961            // Excel semantics: empty cell coerces to 0 in numeric contexts
6962            Ok(LiteralValue::Number(0.0))
6963        }
6964    }
6965}
6966
6967impl<R> crate::traits::RangeResolver for Engine<R>
6968where
6969    R: EvaluationContext,
6970{
6971    fn resolve_range_reference(
6972        &self,
6973        sheet: Option<&str>,
6974        sr: Option<u32>,
6975        sc: Option<u32>,
6976        er: Option<u32>,
6977        ec: Option<u32>,
6978    ) -> Result<Box<dyn crate::traits::Range>, ExcelError> {
6979        // For now, delegate range resolution to the external resolver.
6980        // A future optimization could be to handle this within the graph.
6981        self.resolver.resolve_range_reference(sheet, sr, sc, er, ec)
6982    }
6983}
6984
6985impl<R> crate::traits::NamedRangeResolver for Engine<R>
6986where
6987    R: EvaluationContext,
6988{
6989    fn resolve_named_range_reference(
6990        &self,
6991        name: &str,
6992    ) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
6993        self.resolver.resolve_named_range_reference(name)
6994    }
6995}
6996
6997impl<R> crate::traits::TableResolver for Engine<R>
6998where
6999    R: EvaluationContext,
7000{
7001    fn resolve_table_reference(
7002        &self,
7003        tref: &formualizer_parse::parser::TableReference,
7004    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
7005        self.resolver.resolve_table_reference(tref)
7006    }
7007}
7008
7009impl<R> crate::traits::SourceResolver for Engine<R>
7010where
7011    R: EvaluationContext,
7012{
7013    fn source_scalar_version(&self, name: &str) -> Option<u64> {
7014        self.resolver.source_scalar_version(name)
7015    }
7016
7017    fn resolve_source_scalar(&self, name: &str) -> Result<LiteralValue, ExcelError> {
7018        self.resolver.resolve_source_scalar(name)
7019    }
7020
7021    fn source_table_version(&self, name: &str) -> Option<u64> {
7022        self.resolver.source_table_version(name)
7023    }
7024
7025    fn resolve_source_table(
7026        &self,
7027        name: &str,
7028    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
7029        self.resolver.resolve_source_table(name)
7030    }
7031}
7032
7033// The Engine is a Resolver because it implements the constituent traits.
7034impl<R> crate::traits::Resolver for Engine<R> where R: EvaluationContext {}
7035
7036// The Engine provides functions by delegating to its internal resolver.
7037impl<R> crate::traits::FunctionProvider for Engine<R>
7038where
7039    R: EvaluationContext,
7040{
7041    fn get_function(
7042        &self,
7043        prefix: &str,
7044        name: &str,
7045    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
7046        self.resolver.get_function(prefix, name)
7047    }
7048}
7049
7050// Override EvaluationContext to provide thread pool access
7051impl<R> crate::traits::EvaluationContext for Engine<R>
7052where
7053    R: EvaluationContext,
7054{
7055    fn clock(&self) -> &dyn crate::timezone::ClockProvider {
7056        self.clock.as_ref()
7057    }
7058
7059    fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
7060        self.thread_pool.as_ref()
7061    }
7062
7063    fn cancellation_token(&self) -> Option<Arc<std::sync::atomic::AtomicBool>> {
7064        self.active_cancel_flag.clone()
7065    }
7066
7067    fn chunk_hint(&self) -> Option<usize> {
7068        // Use a simple heuristic from configuration (stripe width * height) as a default hint.
7069        let hint =
7070            (self.config.stripe_height as usize).saturating_mul(self.config.stripe_width as usize);
7071        Some(hint.clamp(1024, 1 << 20)) // clamp between 1K and ~1M
7072    }
7073
7074    fn volatile_level(&self) -> crate::traits::VolatileLevel {
7075        self.config.volatile_level
7076    }
7077
7078    fn workbook_seed(&self) -> u64 {
7079        self.config.workbook_seed
7080    }
7081
7082    fn recalc_epoch(&self) -> u64 {
7083        self.recalc_epoch
7084    }
7085
7086    fn used_rows_for_columns(
7087        &self,
7088        sheet: &str,
7089        start_col: u32,
7090        end_col: u32,
7091    ) -> Option<(u32, u32)> {
7092        // Union Arrow-backed used-region with formula rows that have not been materialized yet.
7093        let sheet_id = self.graph.sheet_id(sheet)?;
7094        let arrow_bounds = self
7095            .sheet_store()
7096            .sheet(sheet)
7097            .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
7098        let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
7099        if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
7100            return Some(bounds);
7101        }
7102        let sc0 = start_col.saturating_sub(1);
7103        let ec0 = end_col.saturating_sub(1);
7104        self.graph
7105            .used_row_bounds_for_columns(sheet_id, sc0, ec0)
7106            .map(|(a0, b0)| (a0 + 1, b0 + 1))
7107    }
7108
7109    fn used_cols_for_rows(&self, sheet: &str, start_row: u32, end_row: u32) -> Option<(u32, u32)> {
7110        // Union Arrow-backed used-region with formula columns that have not been materialized yet.
7111        let sheet_id = self.graph.sheet_id(sheet)?;
7112        let arrow_bounds = self
7113            .sheet_store()
7114            .sheet(sheet)
7115            .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
7116        let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
7117        if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
7118            return Some(bounds);
7119        }
7120        let sr0 = start_row.saturating_sub(1);
7121        let er0 = end_row.saturating_sub(1);
7122        self.graph
7123            .used_col_bounds_for_rows(sheet_id, sr0, er0)
7124            .map(|(a0, b0)| (a0 + 1, b0 + 1))
7125    }
7126
7127    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
7128        let _ = self.graph.sheet_id(sheet)?;
7129        // Excel-like upper bounds; we expose something finite but large.
7130        // Backends may override with real bounds.
7131        Some((1_048_576, 16_384)) // 1048576 rows, 16384 cols (XFD)
7132    }
7133
7134    fn data_snapshot_id(&self) -> u64 {
7135        self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
7136    }
7137
7138    fn backend_caps(&self) -> crate::traits::BackendCaps {
7139        crate::traits::BackendCaps {
7140            streaming: true,
7141            used_region: true,
7142            write: false,
7143            tables: false,
7144            async_stream: false,
7145        }
7146    }
7147
7148    // Flats removed
7149
7150    fn date_system(&self) -> crate::engine::DateSystem {
7151        self.config.date_system
7152    }
7153    /// New: resolve a reference into a RangeView (Phase 2 API)
7154    fn resolve_range_view<'c>(
7155        &'c self,
7156        reference: &ReferenceType,
7157        current_sheet: &str,
7158    ) -> Result<RangeView<'c>, ExcelError> {
7159        match reference {
7160            ReferenceType::External(ext) => {
7161                let name = ext.raw.as_str();
7162                match ext.kind {
7163                    formualizer_parse::parser::ExternalRefKind::Cell { .. } => {
7164                        let Some(source) = self.graph.resolve_source_scalar_entry(name) else {
7165                            return Err(ExcelError::new(ExcelErrorKind::Name)
7166                                .with_message(format!("Undefined name: {name}")));
7167                        };
7168                        let version = source
7169                            .version
7170                            .or_else(|| self.resolver.source_scalar_version(name));
7171                        let v = self.resolve_source_scalar_cached(name, version)?;
7172                        Ok(RangeView::from_owned_rows(
7173                            vec![vec![v]],
7174                            self.config.date_system,
7175                        ))
7176                    }
7177                    formualizer_parse::parser::ExternalRefKind::Range { .. } => {
7178                        let Some(source) = self.graph.resolve_source_table_entry(name) else {
7179                            return Err(ExcelError::new(ExcelErrorKind::Name)
7180                                .with_message(format!("Undefined table: {name}")));
7181                        };
7182                        let version = source
7183                            .version
7184                            .or_else(|| self.resolver.source_table_version(name));
7185                        let table = self.resolve_source_table_cached(name, version)?;
7186                        let spec = Some(formualizer_parse::parser::TableSpecifier::Data);
7187                        self.source_table_to_range_view(table.as_ref(), &spec)
7188                    }
7189                }
7190            }
7191            ReferenceType::Range { .. } => {
7192                let shared = self.resolve_shared_ref(reference, current_sheet)?;
7193                let formualizer_common::SheetRef::Range(range) = shared else {
7194                    return Err(ExcelError::new(ExcelErrorKind::Ref));
7195                };
7196                let sheet_id = match range.sheet {
7197                    formualizer_common::SheetLocator::Id(id) => id,
7198                    _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
7199                };
7200                let sheet_name = self.graph.sheet_name(sheet_id);
7201
7202                let bounded_range = if range.start_row.is_some()
7203                    && range.start_col.is_some()
7204                    && range.end_row.is_some()
7205                    && range.end_col.is_some()
7206                {
7207                    Some(RangeRef::try_from_shared(range.as_ref())?)
7208                } else {
7209                    None
7210                };
7211
7212                let mut sr = bounded_range
7213                    .as_ref()
7214                    .map(|r| r.start.coord.row() + 1)
7215                    .or_else(|| range.start_row.map(|b| b.index + 1));
7216                let mut sc = bounded_range
7217                    .as_ref()
7218                    .map(|r| r.start.coord.col() + 1)
7219                    .or_else(|| range.start_col.map(|b| b.index + 1));
7220                let mut er = bounded_range
7221                    .as_ref()
7222                    .map(|r| r.end.coord.row() + 1)
7223                    .or_else(|| range.end_row.map(|b| b.index + 1));
7224                let mut ec = bounded_range
7225                    .as_ref()
7226                    .map(|r| r.end.coord.col() + 1)
7227                    .or_else(|| range.end_col.map(|b| b.index + 1));
7228
7229                if sr.is_none() && er.is_none() {
7230                    // Full-column reference: anchor at row 1
7231                    let scv = sc.unwrap_or(1);
7232                    let ecv = ec.unwrap_or(scv);
7233                    sr = Some(1);
7234                    if let Some((_, max_r)) = self.used_rows_for_columns(sheet_name, scv, ecv) {
7235                        er = Some(max_r);
7236                    } else if let Some((max_rows, _)) = self.sheet_bounds(sheet_name) {
7237                        er = Some(self.config.max_open_ended_rows);
7238                    }
7239                }
7240                if sc.is_none() && ec.is_none() {
7241                    // Full-row reference: anchor at column 1
7242                    let srv = sr.unwrap_or(1);
7243                    let erv = er.unwrap_or(srv);
7244                    sc = Some(1);
7245                    if let Some((_, max_c)) = self.used_cols_for_rows(sheet_name, srv, erv) {
7246                        ec = Some(max_c);
7247                    } else if let Some((_, max_cols)) = self.sheet_bounds(sheet_name) {
7248                        ec = Some(self.config.max_open_ended_cols);
7249                    }
7250                }
7251                if sr.is_some() && er.is_none() {
7252                    let scv = sc.unwrap_or(1);
7253                    let ecv = ec.unwrap_or(scv);
7254                    if let Some((_, max_r)) = self.used_rows_for_columns(sheet_name, scv, ecv) {
7255                        er = Some(max_r);
7256                    } else if let Some((max_rows, _)) = self.sheet_bounds(sheet_name) {
7257                        er = Some(self.config.max_open_ended_rows);
7258                    }
7259                }
7260                if er.is_some() && sr.is_none() {
7261                    // Open start: anchor at row 1
7262                    sr = Some(1);
7263                }
7264                if sc.is_some() && ec.is_none() {
7265                    let srv = sr.unwrap_or(1);
7266                    let erv = er.unwrap_or(srv);
7267                    if let Some((_, max_c)) = self.used_cols_for_rows(sheet_name, srv, erv) {
7268                        ec = Some(max_c);
7269                    } else if let Some((_, max_cols)) = self.sheet_bounds(sheet_name) {
7270                        ec = Some(self.config.max_open_ended_cols);
7271                    }
7272                }
7273                if ec.is_some() && sc.is_none() {
7274                    // Open start: anchor at column 1
7275                    sc = Some(1);
7276                }
7277
7278                let sr = sr.unwrap_or(1);
7279                let sc = sc.unwrap_or(1);
7280                let er = er.unwrap_or(sr.saturating_sub(1));
7281                let ec = ec.unwrap_or(sc.saturating_sub(1));
7282
7283                if self.force_materialize_range_views {
7284                    if er < sr || ec < sc {
7285                        return Ok(RangeView::from_owned_rows(
7286                            Vec::new(),
7287                            self.config.date_system,
7288                        ));
7289                    }
7290                    let h = (er - sr + 1) as u64;
7291                    let w = (ec - sc + 1) as u64;
7292                    let cell_count = h.saturating_mul(w);
7293                    if cell_count <= self.config.spill.max_spill_cells as u64 {
7294                        let mut rows: Vec<Vec<LiteralValue>> = Vec::with_capacity(h as usize);
7295                        for r in sr..=er {
7296                            let mut rowv: Vec<LiteralValue> = Vec::with_capacity(w as usize);
7297                            for c in sc..=ec {
7298                                rowv.push(
7299                                    self.get_cell_value(sheet_name, r, c)
7300                                        .unwrap_or(LiteralValue::Empty),
7301                                );
7302                            }
7303                            rows.push(rowv);
7304                        }
7305                        return Ok(RangeView::from_owned_rows(rows, self.config.date_system));
7306                    }
7307                }
7308
7309                let Some(asheet) = self.sheet_store().sheet(sheet_name) else {
7310                    return Ok(RangeView::from_owned_rows(
7311                        Vec::new(),
7312                        self.config.date_system,
7313                    ));
7314                };
7315
7316                let rv = if er < sr || ec < sc {
7317                    asheet.range_view(1, 1, 0, 0)
7318                } else {
7319                    let sr0 = sr.saturating_sub(1) as usize;
7320                    let sc0 = sc.saturating_sub(1) as usize;
7321                    let er0 = er.saturating_sub(1) as usize;
7322                    let ec0 = ec.saturating_sub(1) as usize;
7323                    asheet.range_view(sr0, sc0, er0, ec0)
7324                };
7325
7326                Ok(rv)
7327            }
7328            ReferenceType::Cell { .. } => {
7329                let shared = self.resolve_shared_ref(reference, current_sheet)?;
7330                let formualizer_common::SheetRef::Cell(cell) = shared else {
7331                    return Err(ExcelError::new(ExcelErrorKind::Ref));
7332                };
7333                let addr = CellRef::try_from_shared(cell)?;
7334                let sheet_id = addr.sheet_id;
7335                let sheet_name = self.graph.sheet_name(sheet_id);
7336                let row = addr.coord.row() + 1;
7337                let col = addr.coord.col() + 1;
7338
7339                if self.force_materialize_range_views {
7340                    let v = self
7341                        .get_cell_value(sheet_name, row, col)
7342                        .unwrap_or(LiteralValue::Empty);
7343                    return Ok(RangeView::from_owned_rows(
7344                        vec![vec![v]],
7345                        self.config.date_system,
7346                    ));
7347                }
7348
7349                if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
7350                    let r0 = row.saturating_sub(1) as usize;
7351                    let c0 = col.saturating_sub(1) as usize;
7352                    let rv = asheet.range_view(r0, c0, r0, c0);
7353                    Ok(rv)
7354                } else {
7355                    let v = self
7356                        .get_cell_value(sheet_name, row, col)
7357                        .unwrap_or(LiteralValue::Empty);
7358                    Ok(RangeView::from_owned_rows(
7359                        vec![vec![v]],
7360                        self.config.date_system,
7361                    ))
7362                }
7363            }
7364            ReferenceType::NamedRange(name) => {
7365                if let Some(current_id) = self.graph.sheet_id(current_sheet)
7366                    && let Some(named) = self.graph.resolve_name_entry(name, current_id)
7367                {
7368                    match &named.definition {
7369                        NamedDefinition::Cell(cell_ref) => {
7370                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
7371                            if self.force_materialize_range_views {
7372                                let v = self
7373                                    .get_cell_value(
7374                                        sheet_name,
7375                                        cell_ref.coord.row() + 1,
7376                                        cell_ref.coord.col() + 1,
7377                                    )
7378                                    .unwrap_or(LiteralValue::Empty);
7379                                return Ok(RangeView::from_owned_rows(
7380                                    vec![vec![v]],
7381                                    self.config.date_system,
7382                                ));
7383                            } else {
7384                                let asheet = self
7385                                    .sheet_store()
7386                                    .sheet(sheet_name)
7387                                    .expect("Arrow sheet missing for named cell");
7388                                let r0 = cell_ref.coord.row() as usize;
7389                                let c0 = cell_ref.coord.col() as usize;
7390                                let rv = asheet.range_view(r0, c0, r0, c0);
7391                                return Ok(rv);
7392                            }
7393                        }
7394                        NamedDefinition::Range(range_ref) => {
7395                            let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
7396                            let sr = range_ref.start.coord.row() + 1;
7397                            let sc = range_ref.start.coord.col() + 1;
7398                            let er = range_ref.end.coord.row() + 1;
7399                            let ec = range_ref.end.coord.col() + 1;
7400                            if self.force_materialize_range_views {
7401                                let h = (er.saturating_sub(sr) + 1) as u64;
7402                                let w = (ec.saturating_sub(sc) + 1) as u64;
7403                                let cell_count = h.saturating_mul(w);
7404                                if cell_count <= self.config.spill.max_spill_cells as u64 {
7405                                    let mut rows: Vec<Vec<LiteralValue>> =
7406                                        Vec::with_capacity(h as usize);
7407                                    for r in sr..=er {
7408                                        let mut rowv: Vec<LiteralValue> =
7409                                            Vec::with_capacity(w as usize);
7410                                        for c in sc..=ec {
7411                                            rowv.push(
7412                                                self.get_cell_value(sheet_name, r, c)
7413                                                    .unwrap_or(LiteralValue::Empty),
7414                                            );
7415                                        }
7416                                        rows.push(rowv);
7417                                    }
7418                                    return Ok(RangeView::from_owned_rows(
7419                                        rows,
7420                                        self.config.date_system,
7421                                    ));
7422                                }
7423                            }
7424                            let asheet = self
7425                                .sheet_store()
7426                                .sheet(sheet_name)
7427                                .expect("Arrow sheet missing for named range");
7428                            let sr0 = range_ref.start.coord.row() as usize;
7429                            let sc0 = range_ref.start.coord.col() as usize;
7430                            let er0 = range_ref.end.coord.row() as usize;
7431                            let ec0 = range_ref.end.coord.col() as usize;
7432                            let rv = asheet.range_view(sr0, sc0, er0, ec0);
7433                            return Ok(rv);
7434                        }
7435                        NamedDefinition::Literal(v) => {
7436                            return Ok(RangeView::from_owned_rows(
7437                                vec![vec![v.clone()]],
7438                                self.config.date_system,
7439                            ));
7440                        }
7441                        NamedDefinition::Formula { .. } => {
7442                            if let Some(value) = self.graph.get_value(named.vertex) {
7443                                return Ok(RangeView::from_owned_rows(
7444                                    vec![vec![value]],
7445                                    self.config.date_system,
7446                                ));
7447                            }
7448                        }
7449                    }
7450                }
7451
7452                if let Some(source) = self.graph.resolve_source_scalar_entry(name) {
7453                    let version = source
7454                        .version
7455                        .or_else(|| self.resolver.source_scalar_version(name));
7456                    let v = self.resolve_source_scalar_cached(name, version)?;
7457                    return Ok(RangeView::from_owned_rows(
7458                        vec![vec![v]],
7459                        self.config.date_system,
7460                    ));
7461                }
7462
7463                let data = self.resolver.resolve_named_range_reference(name)?;
7464                Ok(RangeView::from_owned_rows(data, self.config.date_system))
7465            }
7466            ReferenceType::Table(tref) => {
7467                if let Some(table) = self.graph.resolve_table_entry(&tref.name) {
7468                    let sheet_name = self.graph.sheet_name(table.range.start.sheet_id);
7469                    let asheet = self
7470                        .sheet_store()
7471                        .sheet(sheet_name)
7472                        .expect("Arrow sheet missing for table reference");
7473
7474                    let sr0 = table.range.start.coord.row() as usize;
7475                    let sc0 = table.range.start.coord.col() as usize;
7476                    let er0 = table.range.end.coord.row() as usize;
7477                    let ec0 = table.range.end.coord.col() as usize;
7478
7479                    let has_totals = table.totals_row;
7480                    let has_headers = table.header_row;
7481                    let data_sr = if has_headers {
7482                        sr0.saturating_add(1)
7483                    } else {
7484                        sr0
7485                    };
7486                    let data_er = if has_totals {
7487                        er0.saturating_sub(1)
7488                    } else {
7489                        er0
7490                    };
7491
7492                    let select = |sr: usize, sc: usize, er: usize, ec: usize| {
7493                        if sr > er || sc > ec {
7494                            asheet.range_view(1, 1, 0, 0)
7495                        } else {
7496                            asheet.range_view(sr, sc, er, ec)
7497                        }
7498                    };
7499
7500                    let av = match &tref.specifier {
7501                        None => {
7502                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
7503                                "Table reference without specifier is unsupported".to_string(),
7504                            ));
7505                        }
7506                        Some(formualizer_parse::parser::TableSpecifier::Column(col)) => {
7507                            let Some(idx) = table.col_index(col) else {
7508                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
7509                                    "Column refers to unknown table column".to_string(),
7510                                ));
7511                            };
7512                            let c0 = sc0 + idx;
7513                            select(data_sr, c0, data_er, c0)
7514                        }
7515                        Some(formualizer_parse::parser::TableSpecifier::ColumnRange(
7516                            start,
7517                            end,
7518                        )) => {
7519                            let Some(si) = table.col_index(start) else {
7520                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
7521                                    "Column range refers to unknown column(s)".to_string(),
7522                                ));
7523                            };
7524                            let Some(ei) = table.col_index(end) else {
7525                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
7526                                    "Column range refers to unknown column(s)".to_string(),
7527                                ));
7528                            };
7529                            let (mut a, mut b) = (si, ei);
7530                            if a > b {
7531                                std::mem::swap(&mut a, &mut b);
7532                            }
7533                            let c_start = sc0 + a;
7534                            let c_end = sc0 + b;
7535                            select(data_sr, c_start, data_er, c_end)
7536                        }
7537                        Some(formualizer_parse::parser::TableSpecifier::All)
7538                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
7539                            formualizer_parse::parser::SpecialItem::All,
7540                        )) => select(sr0, sc0, er0, ec0),
7541                        Some(formualizer_parse::parser::TableSpecifier::Data)
7542                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
7543                            formualizer_parse::parser::SpecialItem::Data,
7544                        )) => select(data_sr, sc0, data_er, ec0),
7545                        Some(formualizer_parse::parser::TableSpecifier::Headers)
7546                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
7547                            formualizer_parse::parser::SpecialItem::Headers,
7548                        )) => {
7549                            if !has_headers {
7550                                asheet.range_view(1, 1, 0, 0)
7551                            } else {
7552                                select(sr0, sc0, sr0, ec0)
7553                            }
7554                        }
7555                        Some(formualizer_parse::parser::TableSpecifier::Totals)
7556                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
7557                            formualizer_parse::parser::SpecialItem::Totals,
7558                        )) => {
7559                            if !has_totals {
7560                                asheet.range_view(1, 1, 0, 0)
7561                            } else {
7562                                select(er0, sc0, er0, ec0)
7563                            }
7564                        }
7565                        Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
7566                            formualizer_parse::parser::SpecialItem::ThisRow,
7567                        )) => {
7568                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
7569                                "@ (This Row) requires table-aware context; not yet supported"
7570                                    .to_string(),
7571                            ));
7572                        }
7573                        Some(formualizer_parse::parser::TableSpecifier::Row(_))
7574                        | Some(formualizer_parse::parser::TableSpecifier::Combination(_)) => {
7575                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
7576                                "Complex structured references not yet supported".to_string(),
7577                            ));
7578                        }
7579                    };
7580
7581                    return Ok(av);
7582                }
7583
7584                if let Some(source) = self.graph.resolve_source_table_entry(&tref.name) {
7585                    let version = source
7586                        .version
7587                        .or_else(|| self.resolver.source_table_version(&tref.name));
7588                    let table = self.resolve_source_table_cached(&tref.name, version)?;
7589                    return self.source_table_to_range_view(table.as_ref(), &tref.specifier);
7590                }
7591
7592                // Fallback: materialize via Resolver::resolve_range_like tranche 1
7593                let boxed = self.resolve_range_like(&ReferenceType::Table(tref.clone()))?;
7594                let owned = boxed.materialise().into_owned();
7595                Ok(RangeView::from_owned_rows(owned, self.config.date_system))
7596            }
7597        }
7598    }
7599
7600    fn resolve_cell_reference_value(
7601        &self,
7602        sheet: Option<&str>,
7603        row: u32,
7604        col: u32,
7605        current_sheet: &str,
7606    ) -> Result<LiteralValue, ExcelError> {
7607        let sheet_name = sheet.unwrap_or(current_sheet);
7608        if self.graph.sheet_id(sheet_name).is_none() {
7609            return Err(ExcelError::new(ExcelErrorKind::Ref));
7610        }
7611        Ok(self
7612            .get_cell_value(sheet_name, row, col)
7613            .unwrap_or(LiteralValue::Empty))
7614    }
7615
7616    fn build_criteria_mask(
7617        &self,
7618        view: &RangeView<'_>,
7619        col_in_view: usize,
7620        pred: &crate::args::CriteriaPredicate,
7621    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
7622        if view.dims().1 == 0 {
7623            return None;
7624        }
7625        // If the view is logically open-ended but the backing sheet has no physical rows,
7626        // treat the mask as empty (0-len) rather than attempting to build a huge mask.
7627        let sheet_rows = view.sheet().nrows as usize;
7628        if sheet_rows == 0 || view.start_row() >= sheet_rows {
7629            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
7630        }
7631        compute_criteria_mask(view, col_in_view, pred)
7632    }
7633
7634    fn build_row_visibility_mask(
7635        &self,
7636        view: &RangeView<'_>,
7637        mode: VisibilityMaskMode,
7638    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
7639        self.build_row_visibility_mask_for_view(view, mode)
7640    }
7641}
7642
7643impl<R> Engine<R>
7644where
7645    R: EvaluationContext,
7646{
7647    fn clear_spill_projection_and_mirror(
7648        &mut self,
7649        anchor_vertex: VertexId,
7650        delta: Option<&mut DeltaCollector>,
7651    ) {
7652        let spill_cells = self
7653            .graph
7654            .spill_cells_for_anchor(anchor_vertex)
7655            .map(|cells| cells.to_vec())
7656            .unwrap_or_default();
7657        if spill_cells.is_empty() {
7658            return;
7659        }
7660
7661        if let Some(delta) = delta
7662            && delta.mode != DeltaMode::Off
7663        {
7664            let empty = LiteralValue::Empty;
7665            for cell in spill_cells.iter() {
7666                let sheet_name = self.graph.sheet_name(cell.sheet_id);
7667                let old = self
7668                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
7669                    .unwrap_or(LiteralValue::Empty);
7670                if old != empty {
7671                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
7672                }
7673            }
7674        }
7675
7676        self.graph.clear_spill_region(anchor_vertex);
7677
7678        if self.config.arrow_storage_enabled
7679            && self.config.delta_overlay_enabled
7680            && self.config.write_formula_overlay_enabled
7681        {
7682            let empty = LiteralValue::Empty;
7683            for cell in spill_cells.iter() {
7684                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
7685                self.mirror_value_to_computed_overlay(
7686                    &sheet_name,
7687                    cell.coord.row() + 1,
7688                    cell.coord.col() + 1,
7689                    &empty,
7690                );
7691            }
7692        }
7693    }
7694
7695    /// Helper: commit spill via shim and mirror resulting cells into Arrow overlay when enabled.
7696    fn commit_spill_and_mirror(
7697        &mut self,
7698        anchor_vertex: VertexId,
7699        targets: &[CellRef],
7700        rows: Vec<Vec<LiteralValue>>,
7701        delta: Option<&mut DeltaCollector>,
7702        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
7703    ) -> Result<(), ExcelError> {
7704        let prev_spill_cells = self
7705            .graph
7706            .spill_cells_for_anchor(anchor_vertex)
7707            .map(|cells| cells.to_vec())
7708            .unwrap_or_default();
7709
7710        if let Some(delta) = delta
7711            && delta.mode != DeltaMode::Off
7712        {
7713            let target_set: FxHashSet<CellRef> = targets.iter().copied().collect();
7714            let empty = LiteralValue::Empty;
7715
7716            // Clears (prev - targets)
7717            for cell in prev_spill_cells.iter() {
7718                if target_set.contains(cell) {
7719                    continue;
7720                }
7721                let sheet_name = self.graph.sheet_name(cell.sheet_id);
7722                let old = self
7723                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
7724                    .unwrap_or(LiteralValue::Empty);
7725                if old != empty {
7726                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
7727                }
7728            }
7729
7730            // Writes (targets)
7731            if !targets.is_empty() && !rows.is_empty() && !rows[0].is_empty() {
7732                let width = rows[0].len();
7733                for (idx, cell) in targets.iter().enumerate() {
7734                    let r_off = idx / width;
7735                    let c_off = idx % width;
7736                    let new = rows
7737                        .get(r_off)
7738                        .and_then(|r| r.get(c_off))
7739                        .cloned()
7740                        .unwrap_or(LiteralValue::Empty);
7741                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
7742                    let old = self
7743                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
7744                        .unwrap_or(LiteralValue::Empty);
7745                    if old != new {
7746                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
7747                    }
7748                }
7749            } else {
7750                // Degenerate shapes: if we have targets but no rows, treat as writing Empty.
7751                for cell in targets.iter() {
7752                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
7753                    let old = self
7754                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
7755                        .unwrap_or(LiteralValue::Empty);
7756                    if !matches!(old, LiteralValue::Empty) {
7757                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
7758                    }
7759                }
7760            }
7761        }
7762
7763        // Commit via shim (releases locks). When the graph value cache is disabled (Arrow-canonical
7764        // values), plan/commit must consult Arrow storage to detect non-empty value blockers.
7765        let arrow_sheets = &self.arrow_sheets;
7766        self.spill_mgr.commit_array_with_value_probe(
7767            &mut self.graph,
7768            anchor_vertex,
7769            targets,
7770            rows.clone(),
7771            overwritable_formulas,
7772            |g, cell| {
7773                let sheet_name = g.sheet_name(cell.sheet_id);
7774                let asheet = arrow_sheets.sheet(sheet_name)?;
7775                let r0 = cell.coord.row() as usize;
7776                let c0 = cell.coord.col() as usize;
7777                let v = asheet.get_cell_value(r0, c0);
7778                if matches!(v, LiteralValue::Empty) {
7779                    None
7780                } else {
7781                    Some(v)
7782                }
7783            },
7784        )?;
7785
7786        if self.config.arrow_storage_enabled
7787            && self.config.delta_overlay_enabled
7788            && self.config.write_formula_overlay_enabled
7789        {
7790            if !prev_spill_cells.is_empty() {
7791                let target_set: FxHashSet<CellRef> = targets.iter().copied().collect();
7792                let empty = LiteralValue::Empty;
7793                for cell in prev_spill_cells.iter() {
7794                    if !target_set.contains(cell) {
7795                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
7796                        self.mirror_value_to_computed_overlay(
7797                            &sheet_name,
7798                            cell.coord.row() + 1,
7799                            cell.coord.col() + 1,
7800                            &empty,
7801                        );
7802                    }
7803                }
7804            }
7805
7806            for (idx, cell) in targets.iter().enumerate() {
7807                if rows.is_empty() || rows[0].is_empty() {
7808                    break;
7809                }
7810                let width = rows[0].len();
7811                let r_off = idx / width;
7812                let c_off = idx % width;
7813                let v = rows[r_off][c_off].clone();
7814                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
7815                self.mirror_value_to_computed_overlay(
7816                    &sheet_name,
7817                    cell.coord.row() + 1,
7818                    cell.coord.col() + 1,
7819                    &v,
7820                );
7821            }
7822        }
7823        Ok(())
7824    }
7825}
7826
7827// ── Effects pipeline (ticket 603) ──────────────────────────────────────────
7828//
7829// Compute → Plan → Apply separation for evaluation side-effects.
7830
7831use crate::engine::effects::Effect;
7832use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog, SpillSnapshot};
7833
7834impl<R> Engine<R>
7835where
7836    R: EvaluationContext,
7837{
7838    /// Plan effects for a single vertex after its value has been computed.
7839    ///
7840    /// This reads graph state but only performs lightweight mutations
7841    /// (`set_kind`, `spill_mgr.reserve`) that are needed for correctness
7842    /// during the planning phase.  Value-changing mutations are deferred to
7843    /// `apply_effect`.
7844    pub(crate) fn plan_vertex_effects(
7845        &mut self,
7846        vertex_id: VertexId,
7847        computed_value: LiteralValue,
7848        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
7849    ) -> Result<Vec<Effect>, ExcelError> {
7850        let kind = self.graph.get_vertex_kind(vertex_id);
7851        let is_formula = matches!(kind, VertexKind::FormulaScalar | VertexKind::FormulaArray);
7852
7853        // If this vertex's cell is currently covered by a spill from a different
7854        // anchor, ignore the computed result.  Formula vertices are exempt:
7855        // they must still evaluate so that overlapping spills produce #SPILL!.
7856        if !is_formula {
7857            if let Some(cell) = self.graph.get_cell_ref(vertex_id)
7858                && let Some(owner) = self.graph.spill_registry_anchor_for_cell(cell)
7859                && owner != vertex_id
7860            {
7861                return Ok(Vec::new());
7862            }
7863            // Non-formula vertices: store value as-is (arrays remain arrays; no spill).
7864            return Ok(vec![Effect::WriteCell {
7865                vertex_id,
7866                value: computed_value,
7867            }]);
7868        }
7869
7870        match computed_value {
7871            LiteralValue::Array(rows) => {
7872                self.plan_array_effects(vertex_id, rows, overwritable_formulas)
7873            }
7874            other => self.plan_scalar_effects(vertex_id, other),
7875        }
7876    }
7877
7878    /// Plan effects for a formula vertex that produced a scalar/error result.
7879    fn plan_scalar_effects(
7880        &self,
7881        vertex_id: VertexId,
7882        value: LiteralValue,
7883    ) -> Result<Vec<Effect>, ExcelError> {
7884        let has_spill = self
7885            .graph
7886            .spill_cells_for_anchor(vertex_id)
7887            .is_some_and(|c| !c.is_empty());
7888
7889        let mut effects = Vec::new();
7890        if has_spill {
7891            effects.push(Effect::SpillClear {
7892                anchor_vertex: vertex_id,
7893            });
7894        }
7895        effects.push(Effect::WriteCell { vertex_id, value });
7896        Ok(effects)
7897    }
7898
7899    /// Plan effects for a formula vertex that produced an array result.
7900    fn plan_array_effects(
7901        &mut self,
7902        vertex_id: VertexId,
7903        rows: Vec<Vec<LiteralValue>>,
7904        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
7905    ) -> Result<Vec<Effect>, ExcelError> {
7906        // Lightweight mutation needed for correct spill-blocking checks.
7907        self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
7908
7909        let anchor = self
7910            .graph
7911            .get_cell_ref(vertex_id)
7912            .expect("cell ref for vertex");
7913        let sheet_id = anchor.sheet_id;
7914        let h = rows.len() as u32;
7915        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
7916
7917        // Hard cap to avoid vertex explosion from huge dynamic arrays.
7918        let spill_cells = (h as u64).saturating_mul(w as u64);
7919        if spill_cells > self.config.spill.max_spill_cells as u64 {
7920            return self.plan_spill_error_effects(vertex_id, "SpillTooLarge", h, w);
7921        }
7922
7923        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity).
7924        const PACKED_MAX_ROW: u32 = 1_048_575;
7925        const PACKED_MAX_COL: u32 = 16_383;
7926        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
7927        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
7928        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
7929            return self.plan_spill_error_effects(vertex_id, "Spill exceeds sheet bounds", h, w);
7930        }
7931
7932        let mut targets = Vec::new();
7933        for r in 0..h {
7934            for c in 0..w {
7935                targets.push(self.graph.make_cell_ref_internal(
7936                    sheet_id,
7937                    anchor.coord.row() + r,
7938                    anchor.coord.col() + c,
7939                ));
7940            }
7941        }
7942
7943        // Region lock via spill manager.
7944        match self.spill_mgr.reserve(
7945            vertex_id,
7946            anchor,
7947            SpillShape { rows: h, cols: w },
7948            SpillMeta {
7949                epoch: self.recalc_epoch,
7950                config: self.config.spill,
7951            },
7952        ) {
7953            Ok(()) => {
7954                // Validate spill region is available.
7955                if let Err(_e) = self.graph.plan_spill_region_allowing_formula_overwrite(
7956                    vertex_id,
7957                    &targets,
7958                    overwritable_formulas,
7959                ) {
7960                    return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
7961                }
7962
7963                // Arrow-canonical mode: graph planning cannot see non-empty value blockers because
7964                // cell values are not cached in the dependency graph. Consult Arrow storage to
7965                // detect occupied cells in the target region.
7966                if !self.graph.value_cache_enabled() {
7967                    let sheet_name = self.graph.sheet_name(sheet_id);
7968                    if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
7969                        for cell in targets.iter() {
7970                            // Allow overwriting the anchor itself.
7971                            if *cell == anchor {
7972                                continue;
7973                            }
7974                            // Allow cells already owned by a spill (plan() validated spill ownership).
7975                            if self.graph.spill_registry_anchor_for_cell(*cell).is_some() {
7976                                continue;
7977                            }
7978                            // Skip formula blockers; plan() handled them (or allowed).
7979                            if let Some(&vid) = self.graph.get_vertex_id_for_address(cell)
7980                                && vid != vertex_id
7981                            {
7982                                match self.graph.get_vertex_kind(vid) {
7983                                    VertexKind::FormulaScalar | VertexKind::FormulaArray => {
7984                                        continue;
7985                                    }
7986                                    _ => {}
7987                                }
7988                            }
7989
7990                            let v = asheet.get_cell_value(
7991                                cell.coord.row() as usize,
7992                                cell.coord.col() as usize,
7993                            );
7994                            if !matches!(v, LiteralValue::Empty) {
7995                                return self.plan_spill_error_effects(
7996                                    vertex_id,
7997                                    "BlockedByValue",
7998                                    h,
7999                                    w,
8000                                );
8001                            }
8002                        }
8003                    }
8004                }
8005
8006                let top_left = rows
8007                    .first()
8008                    .and_then(|r| r.first())
8009                    .cloned()
8010                    .unwrap_or(LiteralValue::Empty);
8011
8012                let mut effects = Vec::new();
8013                // Clear previous spill if any.
8014                let has_prev = self
8015                    .graph
8016                    .spill_cells_for_anchor(vertex_id)
8017                    .is_some_and(|c| !c.is_empty());
8018                if has_prev {
8019                    effects.push(Effect::SpillClear {
8020                        anchor_vertex: vertex_id,
8021                    });
8022                }
8023                effects.push(Effect::SpillCommit {
8024                    anchor_vertex: vertex_id,
8025                    anchor_cell: anchor,
8026                    target_cells: targets,
8027                    values: rows,
8028                });
8029                effects.push(Effect::WriteCell {
8030                    vertex_id,
8031                    value: top_left,
8032                });
8033                Ok(effects)
8034            }
8035            Err(e) => {
8036                let msg = e.message.unwrap_or_else(|| "Spill blocked".to_string());
8037                self.plan_spill_error_effects(vertex_id, &msg, h, w)
8038            }
8039        }
8040    }
8041
8042    /// Build the effect list for a spill that failed validation.
8043    fn plan_spill_error_effects(
8044        &self,
8045        vertex_id: VertexId,
8046        message: &str,
8047        expected_rows: u32,
8048        expected_cols: u32,
8049    ) -> Result<Vec<Effect>, ExcelError> {
8050        let spill_err = ExcelError::new(ExcelErrorKind::Spill)
8051            .with_message(message)
8052            .with_extra(formualizer_common::ExcelErrorExtra::Spill {
8053                expected_rows,
8054                expected_cols,
8055            });
8056        let spill_val = LiteralValue::Error(spill_err);
8057
8058        let effects = vec![
8059            Effect::SpillClear {
8060                anchor_vertex: vertex_id,
8061            },
8062            Effect::WriteCell {
8063                vertex_id,
8064                value: spill_val,
8065            },
8066        ];
8067        Ok(effects)
8068    }
8069
8070    /// Apply a single effect, performing the actual graph mutations.
8071    pub(crate) fn apply_effect(
8072        &mut self,
8073        effect: &Effect,
8074        delta: Option<&mut DeltaCollector>,
8075        log: Option<&mut ChangeLog>,
8076    ) -> Result<(), ExcelError> {
8077        match effect {
8078            Effect::WriteCell { vertex_id, value } => {
8079                self.apply_write_cell(*vertex_id, value, delta);
8080            }
8081            Effect::SpillClear { anchor_vertex } => {
8082                self.apply_spill_clear(*anchor_vertex, delta, log);
8083            }
8084            Effect::SpillCommit {
8085                anchor_vertex,
8086                anchor_cell: _,
8087                target_cells,
8088                values,
8089            } => {
8090                self.apply_spill_commit(*anchor_vertex, target_cells, values.clone(), delta, log)?;
8091            }
8092        }
8093        Ok(())
8094    }
8095
8096    /// Apply a WriteCell effect.
8097    fn apply_write_cell(
8098        &mut self,
8099        vertex_id: VertexId,
8100        value: &LiteralValue,
8101        delta: Option<&mut DeltaCollector>,
8102    ) {
8103        if let Some(d) = delta
8104            && d.mode != DeltaMode::Off
8105            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
8106        {
8107            let sheet_name = self.graph.sheet_name(cell.sheet_id);
8108            let old = self
8109                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
8110                .unwrap_or(LiteralValue::Empty);
8111            if old != *value {
8112                d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
8113            }
8114        }
8115        self.graph.update_vertex_value(vertex_id, value.clone());
8116        self.mirror_vertex_value_to_overlay(vertex_id, value);
8117    }
8118
8119    /// Apply a SpillClear effect.
8120    fn apply_spill_clear(
8121        &mut self,
8122        anchor_vertex: VertexId,
8123        delta: Option<&mut DeltaCollector>,
8124        log: Option<&mut ChangeLog>,
8125    ) {
8126        let spill_cells = self
8127            .graph
8128            .spill_cells_for_anchor(anchor_vertex)
8129            .map(|cells| cells.to_vec())
8130            .unwrap_or_default();
8131        if spill_cells.is_empty() {
8132            return;
8133        }
8134
8135        // Snapshot for ChangeLog before clearing.
8136        let snapshot = if log.is_some() {
8137            self.snapshot_spill_for_anchor(anchor_vertex)
8138        } else {
8139            None
8140        };
8141
8142        // Record delta for cleared cells.
8143        if let Some(d) = delta
8144            && d.mode != DeltaMode::Off
8145        {
8146            let empty = LiteralValue::Empty;
8147            for cell in spill_cells.iter() {
8148                let sheet_name = self.graph.sheet_name(cell.sheet_id);
8149                let old = self
8150                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
8151                    .unwrap_or(LiteralValue::Empty);
8152                if old != empty {
8153                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
8154                }
8155            }
8156        }
8157
8158        self.graph.clear_spill_region(anchor_vertex);
8159
8160        // Mirror Empty to Arrow overlay for cleared cells.
8161        if self.config.arrow_storage_enabled
8162            && self.config.delta_overlay_enabled
8163            && self.config.write_formula_overlay_enabled
8164        {
8165            let empty = LiteralValue::Empty;
8166            for cell in spill_cells.iter() {
8167                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
8168                self.mirror_value_to_computed_overlay(
8169                    &sheet_name,
8170                    cell.coord.row() + 1,
8171                    cell.coord.col() + 1,
8172                    &empty,
8173                );
8174            }
8175        }
8176
8177        // ChangeLog.
8178        if let Some(log) = log
8179            && let Some(old) = snapshot
8180        {
8181            log.record(ChangeEvent::SpillCleared {
8182                anchor: anchor_vertex,
8183                old,
8184            });
8185        }
8186    }
8187
8188    /// Apply a SpillCommit effect.
8189    fn apply_spill_commit(
8190        &mut self,
8191        anchor_vertex: VertexId,
8192        target_cells: &[CellRef],
8193        values: Vec<Vec<LiteralValue>>,
8194        delta: Option<&mut DeltaCollector>,
8195        log: Option<&mut ChangeLog>,
8196    ) -> Result<(), ExcelError> {
8197        // Snapshot for ChangeLog before commit.
8198        let old_snapshot = if log.is_some() {
8199            self.snapshot_spill_for_anchor(anchor_vertex)
8200        } else {
8201            None
8202        };
8203
8204        // Delegate to existing commit_spill_and_mirror for delta + overlay logic.
8205        self.commit_spill_and_mirror(
8206            anchor_vertex,
8207            target_cells,
8208            values.clone(),
8209            delta,
8210            None, // overwritable_formulas already validated in plan phase
8211        )?;
8212
8213        // ChangeLog.
8214        if let Some(log) = log {
8215            log.record(ChangeEvent::SpillCommitted {
8216                anchor: anchor_vertex,
8217                old: old_snapshot,
8218                new: SpillSnapshot {
8219                    target_cells: target_cells.to_vec(),
8220                    values,
8221                },
8222            });
8223        }
8224        Ok(())
8225    }
8226
8227    /// Snapshot a spill region for ChangeLog recording.
8228    ///
8229    /// Extracted from `VertexEditor::snapshot_spill_for_anchor` to be usable
8230    /// without creating a `VertexEditor`.
8231    fn snapshot_spill_for_anchor(&self, anchor: VertexId) -> Option<SpillSnapshot> {
8232        let cells = self.graph.spill_cells_for_anchor(anchor)?.to_vec();
8233        if cells.is_empty() {
8234            return None;
8235        }
8236
8237        let max = self.config.spill.max_spill_cells as usize;
8238        let mut cells = cells;
8239        if cells.len() > max {
8240            cells.truncate(max);
8241        }
8242
8243        let first = *cells.first().expect("non-empty spill cells");
8244        let sheet_name = self.graph.sheet_name(first.sheet_id).to_string();
8245        let row0 = first.coord.row();
8246        let col0 = first.coord.col();
8247
8248        let mut max_row = row0;
8249        let mut max_col = col0;
8250        let mut by_coord: FxHashMap<(u32, u32), LiteralValue> = FxHashMap::default();
8251        for cell in &cells {
8252            max_row = max_row.max(cell.coord.row());
8253            max_col = max_col.max(cell.coord.col());
8254            let v = self
8255                .get_cell_value(&sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
8256                .unwrap_or(LiteralValue::Empty);
8257            by_coord.insert((cell.coord.row(), cell.coord.col()), v);
8258        }
8259
8260        let rows = (max_row - row0 + 1) as usize;
8261        let cols = (max_col - col0 + 1) as usize;
8262        let mut values: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
8263        for r in 0..rows {
8264            let mut row: Vec<LiteralValue> = Vec::with_capacity(cols);
8265            for c in 0..cols {
8266                row.push(
8267                    by_coord
8268                        .get(&(row0 + r as u32, col0 + c as u32))
8269                        .cloned()
8270                        .unwrap_or(LiteralValue::Empty),
8271                );
8272            }
8273            values.push(row);
8274        }
8275
8276        Some(SpillSnapshot {
8277            target_cells: cells,
8278            values,
8279        })
8280    }
8281
8282    // ── Layer evaluation via effects pipeline ──────────────────────────────
8283
8284    /// Evaluate a layer sequentially using the effects pipeline.
8285    fn evaluate_layer_sequential_effects(
8286        &mut self,
8287        layer: &super::scheduler::Layer,
8288    ) -> Result<usize, ExcelError> {
8289        for &vertex_id in &layer.vertices {
8290            let value = match self.evaluate_vertex_immutable(vertex_id) {
8291                Ok(v) => v,
8292                Err(e) => LiteralValue::Error(e),
8293            };
8294            let effects = self.plan_vertex_effects(vertex_id, value, None)?;
8295            for effect in &effects {
8296                self.apply_effect(effect, None, None)?;
8297            }
8298        }
8299        Ok(layer.vertices.len())
8300    }
8301
8302    /// Evaluate a layer sequentially with delta collection via effects pipeline.
8303    fn evaluate_layer_sequential_with_delta_effects(
8304        &mut self,
8305        layer: &super::scheduler::Layer,
8306        delta: &mut DeltaCollector,
8307    ) -> Result<usize, ExcelError> {
8308        for &vertex_id in &layer.vertices {
8309            let value = match self.evaluate_vertex_immutable(vertex_id) {
8310                Ok(v) => v,
8311                Err(e) => LiteralValue::Error(e),
8312            };
8313            let effects = self.plan_vertex_effects(vertex_id, value, None)?;
8314            for effect in &effects {
8315                self.apply_effect(effect, Some(delta), None)?;
8316            }
8317        }
8318        Ok(layer.vertices.len())
8319    }
8320
8321    /// Evaluate a layer sequentially with cancellation support via effects pipeline.
8322    fn evaluate_layer_sequential_cancellable_effects(
8323        &mut self,
8324        layer: &super::scheduler::Layer,
8325        cancel_flag: &AtomicBool,
8326    ) -> Result<usize, ExcelError> {
8327        for (i, &vertex_id) in layer.vertices.iter().enumerate() {
8328            if i % 256 == 0 && cancel_flag.load(Ordering::Relaxed) {
8329                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
8330                    .with_message("Evaluation cancelled within layer".to_string()));
8331            }
8332            let value = match self.evaluate_vertex_immutable(vertex_id) {
8333                Ok(v) => v,
8334                Err(e) => LiteralValue::Error(e),
8335            };
8336            let effects = self.plan_vertex_effects(vertex_id, value, None)?;
8337            for effect in &effects {
8338                self.apply_effect(effect, None, None)?;
8339            }
8340        }
8341        Ok(layer.vertices.len())
8342    }
8343
8344    /// Evaluate a layer sequentially with more frequent cancellation for demand-driven eval.
8345    fn evaluate_layer_sequential_cancellable_demand_driven_effects(
8346        &mut self,
8347        layer: &super::scheduler::Layer,
8348        cancel_flag: &AtomicBool,
8349    ) -> Result<usize, ExcelError> {
8350        for (i, &vertex_id) in layer.vertices.iter().enumerate() {
8351            if i % 128 == 0 && cancel_flag.load(Ordering::Relaxed) {
8352                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
8353                    .with_message("Demand-driven evaluation cancelled within layer".to_string()));
8354            }
8355            let value = match self.evaluate_vertex_immutable(vertex_id) {
8356                Ok(v) => v,
8357                Err(e) => LiteralValue::Error(e),
8358            };
8359            let effects = self.plan_vertex_effects(vertex_id, value, None)?;
8360            for effect in &effects {
8361                self.apply_effect(effect, None, None)?;
8362            }
8363        }
8364        Ok(layer.vertices.len())
8365    }
8366
8367    /// Evaluate a layer in parallel, applying via effects pipeline.
8368    fn evaluate_layer_parallel_effects(
8369        &mut self,
8370        layer: &super::scheduler::Layer,
8371    ) -> Result<usize, ExcelError> {
8372        use rayon::prelude::*;
8373
8374        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
8375
8376        let mut phase1: Vec<VertexId> = Vec::new();
8377        let mut phase2: Vec<VertexId> = Vec::new();
8378        for &vid in &layer.vertices {
8379            if self.graph.get_range_dependencies(vid).is_some() {
8380                phase2.push(vid);
8381            } else {
8382                phase1.push(vid);
8383            }
8384        }
8385
8386        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
8387        let mut applied = 0usize;
8388
8389        for group in [&phase1[..], &phase2[..]] {
8390            if group.is_empty() {
8391                continue;
8392            }
8393
8394            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
8395                thread_pool.install(|| {
8396                    group
8397                        .par_iter()
8398                        .map(
8399                            |&vertex_id| match self.evaluate_vertex_immutable(vertex_id) {
8400                                Ok(v) => Ok((vertex_id, v)),
8401                                Err(e) => Ok((vertex_id, LiteralValue::Error(e))),
8402                            },
8403                        )
8404                        .collect()
8405                });
8406
8407            match results {
8408                Ok(vertex_results) => {
8409                    // Arrays first, then scalars — establishes spill regions before
8410                    // scalar results that might land inside a spilled region.
8411                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
8412                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
8413                    for (vertex_id, result) in vertex_results {
8414                        if matches!(result, LiteralValue::Array(_)) {
8415                            arrays.push((vertex_id, result));
8416                        } else {
8417                            others.push((vertex_id, result));
8418                        }
8419                    }
8420                    for (vertex_id, result) in arrays {
8421                        let effects =
8422                            self.plan_vertex_effects(vertex_id, result, Some(&inflight))?;
8423                        for effect in &effects {
8424                            self.apply_effect(effect, None, None)?;
8425                        }
8426                        applied = applied.saturating_add(1);
8427                    }
8428                    for (vertex_id, result) in others {
8429                        let effects =
8430                            self.plan_vertex_effects(vertex_id, result, Some(&inflight))?;
8431                        for effect in &effects {
8432                            self.apply_effect(effect, None, None)?;
8433                        }
8434                        applied = applied.saturating_add(1);
8435                    }
8436                }
8437                Err(e) => return Err(e),
8438            }
8439        }
8440
8441        Ok(applied)
8442    }
8443
8444    /// Evaluate a layer in parallel with delta collection via effects pipeline.
8445    fn evaluate_layer_parallel_with_delta_effects(
8446        &mut self,
8447        layer: &super::scheduler::Layer,
8448        delta: &mut DeltaCollector,
8449    ) -> Result<usize, ExcelError> {
8450        use rayon::prelude::*;
8451
8452        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
8453
8454        let mut phase1: Vec<VertexId> = Vec::new();
8455        let mut phase2: Vec<VertexId> = Vec::new();
8456        for &vid in &layer.vertices {
8457            if self.graph.get_range_dependencies(vid).is_some() {
8458                phase2.push(vid);
8459            } else {
8460                phase1.push(vid);
8461            }
8462        }
8463
8464        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
8465        let mut applied = 0usize;
8466
8467        for group in [&phase1[..], &phase2[..]] {
8468            if group.is_empty() {
8469                continue;
8470            }
8471            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
8472                thread_pool.install(|| {
8473                    group
8474                        .par_iter()
8475                        .map(
8476                            |&vertex_id| match self.evaluate_vertex_immutable(vertex_id) {
8477                                Ok(v) => Ok((vertex_id, v)),
8478                                Err(e) => Ok((vertex_id, LiteralValue::Error(e))),
8479                            },
8480                        )
8481                        .collect()
8482                });
8483
8484            match results {
8485                Ok(vertex_results) => {
8486                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
8487                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
8488                    for (vertex_id, result) in vertex_results {
8489                        if matches!(result, LiteralValue::Array(_)) {
8490                            arrays.push((vertex_id, result));
8491                        } else {
8492                            others.push((vertex_id, result));
8493                        }
8494                    }
8495                    for (vertex_id, result) in arrays {
8496                        let effects =
8497                            self.plan_vertex_effects(vertex_id, result, Some(&inflight))?;
8498                        for effect in &effects {
8499                            self.apply_effect(effect, Some(delta), None)?;
8500                        }
8501                        applied = applied.saturating_add(1);
8502                    }
8503                    for (vertex_id, result) in others {
8504                        let effects =
8505                            self.plan_vertex_effects(vertex_id, result, Some(&inflight))?;
8506                        for effect in &effects {
8507                            self.apply_effect(effect, Some(delta), None)?;
8508                        }
8509                        applied = applied.saturating_add(1);
8510                    }
8511                }
8512                Err(e) => return Err(e),
8513            }
8514        }
8515
8516        Ok(applied)
8517    }
8518
8519    /// Evaluate a layer in parallel with cancellation support via effects pipeline.
8520    fn evaluate_layer_parallel_cancellable_effects(
8521        &mut self,
8522        layer: &super::scheduler::Layer,
8523        cancel_flag: &AtomicBool,
8524    ) -> Result<usize, ExcelError> {
8525        use rayon::prelude::*;
8526
8527        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
8528
8529        if cancel_flag.load(Ordering::Relaxed) {
8530            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
8531                .with_message("Parallel evaluation cancelled before starting".to_string()));
8532        }
8533
8534        let mut phase1: Vec<VertexId> = Vec::new();
8535        let mut phase2: Vec<VertexId> = Vec::new();
8536        for &vid in &layer.vertices {
8537            if self.graph.get_range_dependencies(vid).is_some() {
8538                phase2.push(vid);
8539            } else {
8540                phase1.push(vid);
8541            }
8542        }
8543
8544        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
8545        let mut applied = 0usize;
8546
8547        for group in [&phase1[..], &phase2[..]] {
8548            if group.is_empty() {
8549                continue;
8550            }
8551
8552            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
8553                thread_pool.install(|| {
8554                    group
8555                        .par_iter()
8556                        .map(|&vertex_id| {
8557                            if cancel_flag.load(Ordering::Relaxed) {
8558                                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
8559                                    .with_message(
8560                                        "Parallel evaluation cancelled during execution"
8561                                            .to_string(),
8562                                    ));
8563                            }
8564                            match self.evaluate_vertex_immutable(vertex_id) {
8565                                Ok(v) => Ok((vertex_id, v)),
8566                                Err(e) => Ok((vertex_id, LiteralValue::Error(e))),
8567                            }
8568                        })
8569                        .collect()
8570                });
8571
8572            match results {
8573                Ok(vertex_results) => {
8574                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
8575                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
8576                    for (vertex_id, result) in vertex_results {
8577                        if matches!(result, LiteralValue::Array(_)) {
8578                            arrays.push((vertex_id, result));
8579                        } else {
8580                            others.push((vertex_id, result));
8581                        }
8582                    }
8583                    for (vertex_id, result) in arrays {
8584                        let effects =
8585                            self.plan_vertex_effects(vertex_id, result, Some(&inflight))?;
8586                        for effect in &effects {
8587                            self.apply_effect(effect, None, None)?;
8588                        }
8589                        applied = applied.saturating_add(1);
8590                    }
8591                    for (vertex_id, result) in others {
8592                        let effects =
8593                            self.plan_vertex_effects(vertex_id, result, Some(&inflight))?;
8594                        for effect in &effects {
8595                            self.apply_effect(effect, None, None)?;
8596                        }
8597                        applied = applied.saturating_add(1);
8598                    }
8599                }
8600                Err(e) => return Err(e),
8601            }
8602        }
8603
8604        Ok(applied)
8605    }
8606
8607    // ── Top-level evaluate_all_logged ───────────────────────────────────────
8608
8609    /// Evaluate all dirty/volatile vertices, recording effects into a ChangeLog.
8610    ///
8611    /// This is the same flow as `evaluate_all` but threads a ChangeLog through
8612    /// every effect application so that spill commits/clears are captured.
8613    pub fn evaluate_all_logged(&mut self, log: &mut ChangeLog) -> Result<EvalResult, ExcelError> {
8614        let _source_cache = self.source_cache_session();
8615        self.validate_deterministic_mode()?;
8616        if self.config.defer_graph_building {
8617            self.build_graph_all()?;
8618        }
8619        self.reset_virtual_dep_telemetry_if_disabled();
8620        let start = crate::instant::FzInstant::now();
8621        let mut computed_vertices = 0;
8622        let mut cycle_errors = 0;
8623
8624        let mut replan_iterations = 0;
8625        const MAX_REPLAN: usize = 5;
8626        let mut telemetry = self
8627            .config
8628            .enable_virtual_dep_telemetry
8629            .then(|| self.start_virtual_dep_telemetry());
8630
8631        log.begin_compound(format!("evaluate_all(epoch={})", self.recalc_epoch));
8632
8633        loop {
8634            let to_evaluate = self.graph.get_evaluation_vertices();
8635            if to_evaluate.is_empty() {
8636                if let Some(t) = telemetry.as_mut()
8637                    && t.bailout_reason.is_none()
8638                {
8639                    t.bailout_reason = Some("no_work");
8640                }
8641                break;
8642            }
8643
8644            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
8645            if let Some(t) = telemetry.as_mut() {
8646                Self::accumulate_schedule_meta(t, &meta);
8647            }
8648
8649            // Handle cycles.
8650            let circ_error = LiteralValue::Error(
8651                ExcelError::new(ExcelErrorKind::Circ)
8652                    .with_message("Circular dependency detected".to_string()),
8653            );
8654            for cycle in &schedule.cycles {
8655                cycle_errors += 1;
8656                for &vertex_id in cycle {
8657                    self.graph
8658                        .update_vertex_value(vertex_id, circ_error.clone());
8659                    self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
8660                }
8661            }
8662
8663            // Evaluate layers.
8664            for layer in &schedule.layers {
8665                computed_vertices += self.evaluate_layer_logged(layer, log)?;
8666            }
8667
8668            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
8669            if let Some(t) = telemetry.as_mut() {
8670                t.changed_vdeps_total += changed_vertices.len();
8671            }
8672            self.graph.clear_dirty_flags(&to_evaluate);
8673            for v in &changed_vertices {
8674                self.graph.set_dirty(*v, true);
8675            }
8676
8677            if changed_vertices.is_empty() {
8678                if let Some(t) = telemetry.as_mut() {
8679                    t.bailout_reason = Some("converged");
8680                }
8681                break;
8682            }
8683            if replan_iterations >= MAX_REPLAN {
8684                if let Some(t) = telemetry.as_mut() {
8685                    t.bailout_reason = Some("max_replan");
8686                }
8687                break;
8688            }
8689            replan_iterations += 1;
8690        }
8691
8692        if let Some(mut t) = telemetry {
8693            t.replan_iterations = replan_iterations;
8694            self.last_virtual_dep_telemetry = t;
8695        }
8696
8697        log.end_compound();
8698
8699        self.graph.redirty_volatiles();
8700        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
8701
8702        Ok(EvalResult {
8703            computed_vertices,
8704            cycle_errors,
8705            elapsed: start.elapsed(),
8706        })
8707    }
8708
8709    /// Evaluate a single layer with ChangeLog recording.
8710    fn evaluate_layer_logged(
8711        &mut self,
8712        layer: &super::scheduler::Layer,
8713        log: &mut ChangeLog,
8714    ) -> Result<usize, ExcelError> {
8715        for &vertex_id in &layer.vertices {
8716            let value = match self.evaluate_vertex_immutable(vertex_id) {
8717                Ok(v) => v,
8718                Err(e) => LiteralValue::Error(e),
8719            };
8720            let effects = self.plan_vertex_effects(vertex_id, value, None)?;
8721            for effect in &effects {
8722                self.apply_effect(effect, None, Some(log))?;
8723            }
8724        }
8725        Ok(layer.vertices.len())
8726    }
8727}