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