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