Skip to main content

formualizer_eval/engine/
eval.rs

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