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