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