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