Skip to main content

formualizer_eval/engine/
eval.rs

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