Skip to main content

formualizer_eval/engine/
eval.rs

1use crate::SheetId;
2use crate::arrow_store::{OverlayFragment, OverlayValue, SheetStore};
3use crate::engine::arena::AstNodeId;
4use crate::engine::eval_delta::{DeltaCollector, DeltaMode, EvalDelta};
5use crate::engine::ingest_pipeline::{DependencyPlanRow, FormulaAstInput};
6use crate::engine::lookup_index_cache::{
7    BuildOutcome, LookupAxis, LookupIndex, LookupIndexCache, LookupIndexCacheReport,
8    LookupIndexKey, estimate_bytes,
9};
10use crate::engine::named_range::{NameScope, NamedDefinition};
11use crate::engine::range_view::RangeView;
12use crate::engine::row_visibility::RowVisibilityState;
13use crate::engine::spill::{RegionLockManager, SpillMeta, SpillShape};
14use crate::engine::virtual_deps::VirtualDepBuilder;
15use crate::engine::{
16    DependencyGraph, EvalConfig, FormulaIngestBatch, FormulaIngestRecord, FormulaIngestReport,
17    FormulaParseDiagnostic, FormulaParsePolicy, FormulaPlaneMode, RowVisibilitySource, Scheduler,
18    VertexId, VertexKind, VisibilityMaskMode,
19};
20use crate::formula_plane::placement::{
21    CandidateAnalysis, FormulaPlacementCandidate, FormulaPlacementResult, PlacementFallbackReason,
22    place_candidate_family_with_analyses, split_candidate_affine_literal_runs,
23};
24use crate::formula_plane::producer::{
25    DirtyProjectionRule, FormulaConsumerReadIndex, FormulaProducerId, FormulaProducerResultIndex,
26    FormulaProducerWork, ProducerDirtyDomain, SpanReadSummary,
27};
28use crate::formula_plane::region_index::{DirtyDomain, Region};
29use crate::formula_plane::runtime::{
30    FormulaPlane, FormulaSpanRef, PlacementCoord, PlacementDomain, ResultRegion,
31};
32use crate::formula_plane::scheduler::{MixedSchedule, build_mixed_schedule};
33#[cfg(test)]
34use crate::formula_plane::span_eval::SpanEvalReport;
35use crate::formula_plane::span_eval::{SpanComputedWriteSink, SpanEvalTask, SpanEvaluator};
36use crate::formula_plane::structural::relocate_ast_for_template_placement;
37use crate::formula_plane::structural_shift::{SpanShiftPlan, StructuralOp, classify_span_for_op};
38use crate::interpreter::Interpreter;
39use crate::reference::{CellRef, Coord, RangeRef};
40use crate::traits::FunctionProvider;
41use crate::traits::{EvaluationContext, ReferenceInfo, Resolver};
42use chrono::Timelike;
43use formualizer_common::{
44    CoordBuildHasher, LiteralValue, col_letters_from_1based, parse_a1_1based,
45};
46use formualizer_parse::parser::ReferenceType;
47use formualizer_parse::{ASTNode, ASTNodeType, ExcelError, ExcelErrorKind};
48use rayon::ThreadPoolBuilder;
49use rustc_hash::{FxHashMap, FxHashSet};
50use std::collections::{BTreeMap, BTreeSet, VecDeque};
51use std::sync::Arc;
52use std::sync::atomic::{AtomicBool, Ordering};
53
54type StagedFormulaEntry = (u32, u32, String);
55type StagedFormulaMap = std::collections::HashMap<String, Vec<StagedFormulaEntry>>;
56
57fn producer_dirty_to_span_dirty(
58    dirty: ProducerDirtyDomain,
59    span_ref: FormulaSpanRef,
60) -> DirtyDomain {
61    match dirty {
62        ProducerDirtyDomain::Whole => DirtyDomain::WholeSpan(span_ref),
63        ProducerDirtyDomain::Cells(cells) => DirtyDomain::Cells(cells),
64        ProducerDirtyDomain::Regions(regions) => DirtyDomain::Regions(regions),
65    }
66}
67type PreparedFormulaBatches = Vec<FormulaIngestBatch>;
68type StagedFormulaBatches = Vec<(String, Vec<StagedFormulaEntry>)>;
69type FormulaPlaneMixedScheduleBuild = (
70    MixedSchedule,
71    BTreeMap<crate::formula_plane::runtime::FormulaSpanId, FormulaSpanRef>,
72    u64,
73    Vec<VertexId>,
74);
75
76type PlannedFormulaMaterialize = BTreeMap<String, Vec<(u32, u32, AstNodeId, DependencyPlanRow)>>;
77
78// Computed-write coalescing pays a fixed grouping/planning cost. For very narrow
79// layers there is not enough work to amortize it, and the direct point-write path
80// is faster while preserving the same visibility semantics.
81const COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH: usize = 8;
82
83#[derive(Debug, Clone, PartialEq)]
84pub(crate) enum ComputedWrite {
85    Cell {
86        seq: u64,
87        sheet_id: SheetId,
88        row0: u32,
89        col0: u32,
90        value: OverlayValue,
91    },
92    Rect {
93        seq: u64,
94        sheet_id: SheetId,
95        sr0: u32,
96        sc0: u32,
97        values: Vec<Vec<OverlayValue>>,
98    },
99}
100
101impl ComputedWrite {
102    #[inline]
103    pub(crate) fn seq(&self) -> u64 {
104        match self {
105            ComputedWrite::Cell { seq, .. } | ComputedWrite::Rect { seq, .. } => *seq,
106        }
107    }
108}
109
110#[derive(Debug, Default)]
111pub(crate) struct ComputedWriteBuffer {
112    writes: Vec<ComputedWrite>,
113    next_seq: u64,
114    estimated_bytes: usize,
115}
116
117impl ComputedWriteBuffer {
118    const ENTRY_BASE_BYTES: usize = 32;
119
120    #[inline]
121    pub(crate) fn is_empty(&self) -> bool {
122        self.writes.is_empty()
123    }
124
125    #[inline]
126    pub(crate) fn len(&self) -> usize {
127        self.writes.len()
128    }
129
130    #[inline]
131    pub(crate) fn estimated_bytes(&self) -> usize {
132        self.estimated_bytes
133    }
134
135    #[inline]
136    pub(crate) fn writes(&self) -> &[ComputedWrite] {
137        &self.writes
138    }
139
140    pub(crate) fn push_cell(
141        &mut self,
142        sheet_id: SheetId,
143        row0: u32,
144        col0: u32,
145        value: OverlayValue,
146    ) {
147        let seq = self.next_sequence();
148        self.estimated_bytes = self
149            .estimated_bytes
150            .saturating_add(Self::estimate_value_bytes(&value));
151        self.writes.push(ComputedWrite::Cell {
152            seq,
153            sheet_id,
154            row0,
155            col0,
156            value,
157        });
158    }
159
160    pub(crate) fn push_rect(
161        &mut self,
162        sheet_id: SheetId,
163        sr0: u32,
164        sc0: u32,
165        values: Vec<Vec<OverlayValue>>,
166    ) {
167        let seq = self.next_sequence();
168        let added = values
169            .iter()
170            .flat_map(|row| row.iter())
171            .map(Self::estimate_value_bytes)
172            .fold(0usize, usize::saturating_add);
173        self.estimated_bytes = self.estimated_bytes.saturating_add(added);
174        self.writes.push(ComputedWrite::Rect {
175            seq,
176            sheet_id,
177            sr0,
178            sc0,
179            values,
180        });
181    }
182
183    pub(crate) fn clear(&mut self) {
184        self.writes.clear();
185        self.estimated_bytes = 0;
186    }
187
188    fn take_writes(&mut self) -> Vec<ComputedWrite> {
189        self.estimated_bytes = 0;
190        std::mem::take(&mut self.writes)
191    }
192
193    fn next_sequence(&mut self) -> u64 {
194        let seq = self.next_seq;
195        self.next_seq = self.next_seq.wrapping_add(1);
196        seq
197    }
198
199    #[inline]
200    fn estimate_value_bytes(value: &OverlayValue) -> usize {
201        Self::ENTRY_BASE_BYTES.saturating_add(value.estimated_payload_bytes())
202    }
203}
204
205#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
206struct ComputedWriteChunkKey {
207    sheet_id: SheetId,
208    col0: u32,
209    chunk_idx: usize,
210    chunk_start_row0: u32,
211}
212
213#[derive(Debug, Clone, PartialEq)]
214pub(crate) struct ComputedWriteChunkEntryPlan {
215    pub(crate) row_in_chunk: usize,
216    pub(crate) seq: u64,
217    pub(crate) value: OverlayValue,
218}
219
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub(crate) enum ComputedWriteChunkPlanShape {
222    Point,
223    SparseOffsets {
224        entries: usize,
225        span_len: usize,
226    },
227    DenseRange {
228        start: usize,
229        len: usize,
230    },
231    RunRange {
232        start: usize,
233        len: usize,
234        runs: usize,
235    },
236}
237
238#[derive(Debug, Clone, PartialEq)]
239pub(crate) struct ComputedWriteChunkPlan {
240    pub(crate) sheet_id: SheetId,
241    pub(crate) col0: u32,
242    pub(crate) chunk_idx: usize,
243    pub(crate) chunk_start_row0: u32,
244    pub(crate) entries: Vec<ComputedWriteChunkEntryPlan>,
245    pub(crate) shape: ComputedWriteChunkPlanShape,
246}
247
248#[derive(Debug, Clone, Default, PartialEq)]
249pub(crate) struct ComputedWriteCoalescingPlan {
250    pub(crate) chunks: Vec<ComputedWriteChunkPlan>,
251    pub(crate) input_cells: usize,
252    pub(crate) coalesced_cells: usize,
253    pub(crate) overwritten_cells: usize,
254}
255
256impl ComputedWriteCoalescingPlan {
257    #[inline]
258    pub(crate) fn is_empty(&self) -> bool {
259        self.chunks.is_empty()
260    }
261}
262
263impl ComputedWriteChunkPlan {
264    fn from_group(
265        key: ComputedWriteChunkKey,
266        mut entries: Vec<ComputedWriteChunkEntryPlan>,
267    ) -> (Self, usize) {
268        entries.sort_by_key(|entry| (entry.row_in_chunk, entry.seq));
269        let input_len = entries.len();
270        let mut coalesced: Vec<ComputedWriteChunkEntryPlan> = Vec::with_capacity(input_len);
271        for entry in entries {
272            if let Some(prev) = coalesced.last_mut()
273                && prev.row_in_chunk == entry.row_in_chunk
274            {
275                *prev = entry;
276                continue;
277            }
278            coalesced.push(entry);
279        }
280        let overwritten = input_len.saturating_sub(coalesced.len());
281        let shape = Self::classify_shape(&coalesced);
282        (
283            Self {
284                sheet_id: key.sheet_id,
285                col0: key.col0,
286                chunk_idx: key.chunk_idx,
287                chunk_start_row0: key.chunk_start_row0,
288                entries: coalesced,
289                shape,
290            },
291            overwritten,
292        )
293    }
294
295    fn classify_shape(entries: &[ComputedWriteChunkEntryPlan]) -> ComputedWriteChunkPlanShape {
296        debug_assert!(!entries.is_empty());
297        if entries.len() == 1 {
298            return ComputedWriteChunkPlanShape::Point;
299        }
300
301        let start = entries[0].row_in_chunk;
302        let end = entries[entries.len() - 1].row_in_chunk;
303        let span_len = end.saturating_sub(start).saturating_add(1);
304        if span_len != entries.len() {
305            return ComputedWriteChunkPlanShape::SparseOffsets {
306                entries: entries.len(),
307                span_len,
308            };
309        }
310
311        let runs = Self::run_count(entries);
312        if runs < entries.len() {
313            ComputedWriteChunkPlanShape::RunRange {
314                start,
315                len: entries.len(),
316                runs,
317            }
318        } else {
319            ComputedWriteChunkPlanShape::DenseRange {
320                start,
321                len: entries.len(),
322            }
323        }
324    }
325
326    fn run_count(entries: &[ComputedWriteChunkEntryPlan]) -> usize {
327        let mut runs = 0usize;
328        let mut prev: Option<&OverlayValue> = None;
329        for entry in entries {
330            if prev != Some(&entry.value) {
331                runs = runs.saturating_add(1);
332                prev = Some(&entry.value);
333            }
334        }
335        runs
336    }
337}
338
339pub struct Engine<R> {
340    pub(crate) graph: DependencyGraph,
341    resolver: R,
342    pub config: EvalConfig,
343    workbook_load_limits: crate::engine::WorkbookLoadLimits,
344    clock: Arc<dyn crate::timezone::ClockProvider>,
345    thread_pool: Option<Arc<rayon::ThreadPool>>,
346    pub recalc_epoch: u64,
347    snapshot_id: std::sync::atomic::AtomicU64,
348    topology_epoch: u64,
349    cached_static_schedule: Option<CachedScheduleEntry>,
350    spill_mgr: ShimSpillManager,
351    /// Arrow-backed storage for sheet values (Phase A)
352    arrow_sheets: SheetStore,
353    /// True if any edit after bulk load; disables Arrow reads for parity
354    has_edited: bool,
355    /// Overlay compaction counter (Phase C instrumentation)
356    overlay_compactions: u64,
357
358    // Overlay memory observability / budget (ticket 503)
359    computed_overlay_bytes_estimate: usize,
360    computed_overlay_mirroring_disabled: bool,
361    /// When true, RangeView resolution materializes from graph/Arrow base per-cell.
362    /// This preserves correctness if we stop mirroring formula/spill outputs into computed overlays.
363    pub(crate) force_materialize_range_views: bool,
364    // Pass-scoped cache for Arrow used-row bounds per column
365    row_bounds_cache: std::sync::RwLock<Option<RowBoundsCache>>,
366    // Snapshot-scoped final used-axis bounds for open-ended references.
367    used_axis_bounds_cache: std::sync::RwLock<Option<UsedAxisBoundsCache>>,
368    lookup_index_cache: LookupIndexCache,
369    source_cache: Arc<std::sync::RwLock<SourceCache>>,
370    /// Staged formulas by sheet when `defer_graph_building` is enabled.
371    staged_formulas: StagedFormulaMap,
372    /// Per-sheet row visibility sidecar state.
373    row_visibility: FxHashMap<SheetId, RowVisibilityState>,
374    /// Cached row visibility masks keyed by sheet/span/mode/version.
375    row_visibility_mask_cache: std::sync::RwLock<
376        FxHashMap<VisibilityMaskCacheKey, std::sync::Arc<arrow_array::BooleanArray>>,
377    >,
378    /// Non-fatal malformed formula diagnostics captured during ingest/graph-build.
379    formula_parse_diagnostics: Vec<FormulaParseDiagnostic>,
380    /// Last centralized formula ingest report.
381    last_formula_ingest_report: Option<FormulaIngestReport>,
382    /// Aggregate centralized formula ingest report for this engine.
383    formula_ingest_report_total: FormulaIngestReport,
384    /// Transient cancellation flag used during evaluation
385    active_cancel_flag: Option<Arc<AtomicBool>>,
386
387    /// Engine-level action depth.
388    ///
389    /// Ticket 614 introduces `Engine::action` as a stable, commit-only transaction surface.
390    /// Nested actions are currently disallowed (deterministic rule) and will return an error.
391    action_depth: u32,
392
393    // Phase 3b virtual-dependency convergence telemetry
394    last_virtual_dep_telemetry: VirtualDepTelemetry,
395    virtual_dep_fallback_activations: u64,
396
397    /// FormulaPlane authority `indexes_epoch` observed by the most recent
398    /// successful `evaluate_all` pass. Used to schedule whole-span work for
399    /// any active span the engine has not yet evaluated under the current
400    /// indexes generation; subsequent passes use bounded dirty closures.
401    formula_plane_indexes_epoch_seen: u64,
402
403    #[cfg(test)]
404    last_formula_plane_span_eval_report: Option<SpanEvalReport>,
405}
406
407/// Minimal edit surface used by `Engine::action`.
408///
409/// This wrapper is intentionally thin for ticket 614 (commit-only): it delegates to existing
410/// `Engine` edit methods and does not create changelog boundaries or implement rollback.
411impl<R: EvaluationContext> Engine<R> {
412    pub(crate) fn ingest_pipeline(&mut self) -> crate::engine::ingest_pipeline::IngestPipeline<'_> {
413        self.graph.ingest_pipeline(&self.resolver)
414    }
415}
416
417pub struct EngineAction<'a, R>
418where
419    R: EvaluationContext,
420{
421    engine: &'a mut Engine<R>,
422    name: String,
423    // Optional external ChangeLog pointer used by `Engine::action_with_logger`.
424    // Stored as a raw pointer to avoid creating aliasing `&mut` borrows alongside `&mut Engine`.
425    log: Option<*mut crate::engine::ChangeLog>,
426    // Optional Arrow undo journal used by `Engine::action_atomic`.
427    // Stored as a raw pointer to avoid aliasing issues with `&mut Engine`.
428    arrow_undo: Option<*mut crate::engine::ArrowUndoBatch>,
429    // True when this EngineAction must enforce conservative atomic transaction policy.
430    atomic_policy: bool,
431}
432
433impl<'a, R> EngineAction<'a, R>
434where
435    R: EvaluationContext,
436{
437    #[inline]
438    fn addr_for(&mut self, sheet: &str, row: u32, col: u32) -> crate::reference::CellRef {
439        let sheet_id = self.engine.graph.sheet_id_mut(sheet);
440        let coord = crate::reference::Coord::from_excel(row, col, true, true);
441        crate::reference::CellRef::new(sheet_id, coord)
442    }
443
444    #[inline]
445    pub fn name(&self) -> &str {
446        &self.name
447    }
448
449    #[inline]
450    pub fn set_cell_value(
451        &mut self,
452        sheet: &str,
453        row: u32,
454        col: u32,
455        value: LiteralValue,
456    ) -> Result<(), crate::engine::EditorError> {
457        if self.log.is_some() {
458            let old_value = self.engine.read_cell_value(sheet, row, col);
459            let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
460            let addr = self.addr_for(sheet, row, col);
461            let Some(log_ptr) = self.log else {
462                return Err(crate::engine::EditorError::TransactionFailed {
463                    reason: "action_with_logger: missing ChangeLog".to_string(),
464                });
465            };
466
467            // For atomic journal mode, record computed overlay effects for this cell.
468            // Delta-overlay undo is recorded semantically based on old_value/old_formula.
469            let old_comp = if self.arrow_undo.is_some() {
470                self.engine.read_computed_overlay_cell(sheet, row, col)
471            } else {
472                None
473            };
474
475            self.engine.demote_span_containing_cell_for_write(
476                addr.sheet_id,
477                addr.coord.row(),
478                addr.coord.col(),
479            )?;
480            if old_formula.is_none() {
481                old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
482            }
483
484            let delta_old_sem = if old_formula.is_some() {
485                None
486            } else {
487                Some(old_value.clone().unwrap_or(LiteralValue::Empty))
488            };
489
490            let start_len = unsafe { (&*log_ptr).len() };
491
492            // Safety: `log_ptr` comes from a unique `&mut ChangeLog` in `Engine::action_with_logger`.
493            let log = unsafe { &mut *log_ptr };
494            self.engine.edit_with_logger(log, |editor| {
495                editor.set_cell_value(addr, value.clone());
496            });
497            log.patch_last_cell_event_old_state(addr, old_value.clone(), old_formula.clone());
498            self.engine
499                .record_formula_plane_structural_change(StructuralScope::Cell {
500                    sheet: addr.sheet_id,
501                    row: addr.coord.row(),
502                    col: addr.coord.col(),
503                });
504
505            if let Some(undo_ptr) = self.arrow_undo {
506                // 1) Spill snapshot operations (computed overlay rect restore).
507                let new_events = &unsafe { (&*log_ptr).events() }[start_len..];
508                let undo = unsafe { &mut *undo_ptr };
509                self.engine
510                    .record_spill_ops_into_arrow_undo(undo, new_events);
511
512                // 2) Delta/computed overlay single-cell deltas.
513                let new_comp = self.engine.read_computed_overlay_cell(sheet, row, col);
514                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
515                let row0 = row.saturating_sub(1);
516                let col0 = col.saturating_sub(1);
517                let delta_new_sem = Some(value.clone());
518                undo.record_delta_cell(sheet_id, row0, col0, delta_old_sem, delta_new_sem);
519                undo.record_computed_cell(sheet_id, row0, col0, old_comp, new_comp);
520            }
521            Ok(())
522        } else {
523            self.engine
524                .set_cell_value(sheet, row, col, value)
525                .map_err(crate::engine::EditorError::from)
526        }
527    }
528
529    #[inline]
530    pub fn set_cell_formula(
531        &mut self,
532        sheet: &str,
533        row: u32,
534        col: u32,
535        ast: ASTNode,
536    ) -> Result<(), crate::engine::EditorError> {
537        if self.log.is_some() {
538            let old_value = self.engine.read_cell_value(sheet, row, col);
539            let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
540            let addr = self.addr_for(sheet, row, col);
541            let Some(log_ptr) = self.log else {
542                return Err(crate::engine::EditorError::TransactionFailed {
543                    reason: "action_with_logger: missing ChangeLog".to_string(),
544                });
545            };
546
547            self.engine.demote_span_containing_cell_for_write(
548                addr.sheet_id,
549                addr.coord.row(),
550                addr.coord.col(),
551            )?;
552            if old_formula.is_none() {
553                old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
554            }
555            let delta_old = if self.arrow_undo.is_some() {
556                if old_formula.is_some() {
557                    None
558                } else {
559                    Some(old_value.clone().unwrap_or(LiteralValue::Empty))
560                }
561            } else {
562                None
563            };
564            let start_len = unsafe { (&*log_ptr).len() };
565
566            // Safety: `log_ptr` comes from a unique `&mut ChangeLog` in `Engine::action_with_logger`.
567            let log = unsafe { &mut *log_ptr };
568            self.engine.edit_with_logger(log, |editor| {
569                editor.set_cell_formula(addr, ast.clone());
570            });
571            log.patch_last_cell_event_old_state(addr, old_value, old_formula);
572            self.engine
573                .record_formula_plane_structural_change(StructuralScope::Cell {
574                    sheet: addr.sheet_id,
575                    row: addr.coord.row(),
576                    col: addr.coord.col(),
577                });
578
579            if let Some(undo_ptr) = self.arrow_undo {
580                let new_events = &unsafe { (&*log_ptr).events() }[start_len..];
581                let undo = unsafe { &mut *undo_ptr };
582                self.engine
583                    .record_spill_ops_into_arrow_undo(undo, new_events);
584                let delta_new: Option<LiteralValue> = None;
585                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
586                let row0 = row.saturating_sub(1);
587                let col0 = col.saturating_sub(1);
588                undo.record_delta_cell(sheet_id, row0, col0, delta_old, delta_new);
589            }
590            Ok(())
591        } else {
592            self.engine
593                .set_cell_formula(sheet, row, col, ast)
594                .map_err(crate::engine::EditorError::from)
595        }
596    }
597
598    #[inline]
599    pub fn set_row_hidden(
600        &mut self,
601        sheet: &str,
602        row_1based: u32,
603        hidden: bool,
604        source: RowVisibilitySource,
605    ) -> Result<(), crate::engine::EditorError> {
606        if self.log.is_some() {
607            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
608            let row0 = Engine::<R>::normalize_row_1based(row_1based)?;
609            let old_hidden = self
610                .engine
611                .row_visibility
612                .get(&sheet_id)
613                .map(|state| state.is_row_hidden(row0, Some(source)))
614                .unwrap_or(false);
615            if old_hidden == hidden {
616                return Ok(());
617            }
618
619            let _ = self
620                .engine
621                .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
622
623            let Some(log_ptr) = self.log else {
624                return Err(crate::engine::EditorError::TransactionFailed {
625                    reason: "action_with_logger: missing ChangeLog".to_string(),
626                });
627            };
628            unsafe { &mut *log_ptr }.record(crate::engine::ChangeEvent::SetRowVisibility {
629                sheet_id,
630                row0,
631                source,
632                old_hidden,
633                new_hidden: hidden,
634            });
635
636            Ok(())
637        } else {
638            self.engine
639                .set_row_hidden(sheet, row_1based, hidden, source)
640        }
641    }
642
643    #[inline]
644    pub fn set_rows_hidden(
645        &mut self,
646        sheet: &str,
647        start_row_1based: u32,
648        end_row_1based: u32,
649        hidden: bool,
650        source: RowVisibilitySource,
651    ) -> Result<(), crate::engine::EditorError> {
652        if self.log.is_some() {
653            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
654            let (start_row0, end_row0) =
655                Engine::<R>::normalize_row_range_1based(start_row_1based, end_row_1based)?;
656
657            let Some(log_ptr) = self.log else {
658                return Err(crate::engine::EditorError::TransactionFailed {
659                    reason: "action_with_logger: missing ChangeLog".to_string(),
660                });
661            };
662            let log = unsafe { &mut *log_ptr };
663
664            for row0 in start_row0..=end_row0 {
665                let old_hidden = self
666                    .engine
667                    .row_visibility
668                    .get(&sheet_id)
669                    .map(|state| state.is_row_hidden(row0, Some(source)))
670                    .unwrap_or(false);
671                if old_hidden == hidden {
672                    continue;
673                }
674
675                let _ = self
676                    .engine
677                    .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
678
679                log.record(crate::engine::ChangeEvent::SetRowVisibility {
680                    sheet_id,
681                    row0,
682                    source,
683                    old_hidden,
684                    new_hidden: hidden,
685                });
686            }
687
688            Ok(())
689        } else {
690            self.engine
691                .set_rows_hidden(sheet, start_row_1based, end_row_1based, hidden, source)
692        }
693    }
694
695    #[inline]
696    pub fn insert_rows(
697        &mut self,
698        sheet: &str,
699        before: u32,
700        count: u32,
701    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
702        if self.log.is_some() {
703            let Some(log_ptr) = self.log else {
704                return Err(crate::engine::EditorError::TransactionFailed {
705                    reason: "action_atomic: missing ChangeLog".to_string(),
706                });
707            };
708
709            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
710            let before0 = before.saturating_sub(1);
711            let op = StructuralOp::InsertRows {
712                sheet_id,
713                before: before0,
714                count,
715            };
716            self.engine.demote_spans_for_structural_op(
717                op,
718                Engine::<R>::structural_row_region(sheet_id, before0),
719            )?;
720
721            // Graph structural insert (logged) - no snapshot bump.
722            let summary = {
723                let log = unsafe { &mut *log_ptr };
724                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
725                    Ok(crate::engine::ShiftSummary::default());
726                self.engine.edit_with_logger(log, |editor| {
727                    out = editor.insert_rows(sheet_id, before0, count);
728                });
729                out?
730            };
731
732            // Arrow insert (truth) + undo op.
733            self.engine.ensure_arrow_sheet(sheet);
734            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
735                asheet.insert_rows(before0 as usize, count as usize);
736            }
737            self.engine
738                .shift_row_visibility_insert(sheet_id, before0, count);
739            if let Some(undo_ptr) = self.arrow_undo {
740                unsafe { &mut *undo_ptr }.record_insert_rows(sheet_id, before0, count);
741            }
742            Ok(summary)
743        } else {
744            self.engine.insert_rows(sheet, before, count)
745        }
746    }
747
748    #[inline]
749    pub fn delete_rows(
750        &mut self,
751        sheet: &str,
752        start: u32,
753        count: u32,
754    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
755        if self.atomic_policy {
756            return Err(crate::engine::EditorError::TransactionUnsupported {
757                reason:
758                    "delete_rows is not supported inside atomic actions (conservative rollback policy)"
759                        .to_string(),
760            });
761        }
762        self.engine.delete_rows(sheet, start, count)
763    }
764
765    #[inline]
766    pub fn insert_columns(
767        &mut self,
768        sheet: &str,
769        before: u32,
770        count: u32,
771    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
772        if self.log.is_some() {
773            let Some(log_ptr) = self.log else {
774                return Err(crate::engine::EditorError::TransactionFailed {
775                    reason: "action_atomic: missing ChangeLog".to_string(),
776                });
777            };
778
779            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
780            let before0 = before.saturating_sub(1);
781            let op = StructuralOp::InsertColumns {
782                sheet_id,
783                before: before0,
784                count,
785            };
786            self.engine.demote_spans_for_structural_op(
787                op,
788                Engine::<R>::structural_col_region(sheet_id, before0),
789            )?;
790
791            let summary = {
792                let log = unsafe { &mut *log_ptr };
793                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
794                    Ok(crate::engine::ShiftSummary::default());
795                self.engine.edit_with_logger(log, |editor| {
796                    out = editor.insert_columns(sheet_id, before0, count);
797                });
798                out?
799            };
800
801            self.engine.ensure_arrow_sheet(sheet);
802            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
803                asheet.insert_columns(before0 as usize, count as usize);
804            }
805            if let Some(undo_ptr) = self.arrow_undo {
806                unsafe { &mut *undo_ptr }.record_insert_cols(sheet_id, before0, count);
807            }
808            Ok(summary)
809        } else {
810            self.engine.insert_columns(sheet, before, count)
811        }
812    }
813
814    #[inline]
815    pub fn delete_columns(
816        &mut self,
817        sheet: &str,
818        start: u32,
819        count: u32,
820    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
821        if self.atomic_policy {
822            return Err(crate::engine::EditorError::TransactionUnsupported {
823                reason:
824                    "delete_columns is not supported inside atomic actions (conservative rollback policy)"
825                        .to_string(),
826            });
827        }
828        self.engine.delete_columns(sheet, start, count)
829    }
830
831    /// Start an action from within an action.
832    ///
833    /// Nested actions are currently disallowed (ticket 614), so this will return a
834    /// `EditorError::TransactionFailed` while an outer action is active.
835    #[inline]
836    pub fn action<T>(
837        &mut self,
838        name: impl AsRef<str>,
839        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
840    ) -> Result<T, crate::engine::EditorError> {
841        self.engine.action(name, f)
842    }
843}
844
845struct ActionDepthGuard<'a, R> {
846    engine: *mut Engine<R>,
847    _marker: std::marker::PhantomData<&'a mut Engine<R>>,
848}
849
850impl<'a, R> Drop for ActionDepthGuard<'a, R> {
851    fn drop(&mut self) {
852        // Safety: the guard is created from a unique `&mut Engine` borrow and lives no longer
853        // than the surrounding `Engine::action` call.
854        unsafe {
855            let e = &mut *self.engine;
856            e.action_depth = e.action_depth.saturating_sub(1);
857        }
858    }
859}
860
861#[derive(Default)]
862struct SourceCache {
863    scalars: FxHashMap<(String, Option<u64>), LiteralValue>,
864    tables: FxHashMap<(String, Option<u64>), Arc<dyn crate::traits::Table>>,
865}
866
867#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
868struct VisibilityMaskCacheKey {
869    sheet_id: SheetId,
870    start_row0: u32,
871    end_row0: u32,
872    mode: VisibilityMaskMode,
873    version: u64,
874}
875
876#[derive(Debug, Clone, Copy, PartialEq, Eq)]
877enum StructuralScope {
878    Cell { sheet: SheetId, row: u32, col: u32 },
879    Region(Region),
880    Sheet(SheetId),
881    RemovedSheet(SheetId),
882    AllSheets,
883}
884
885struct SourceCacheSession {
886    cache: Arc<std::sync::RwLock<SourceCache>>,
887}
888
889impl Drop for SourceCacheSession {
890    fn drop(&mut self) {
891        if let Ok(mut g) = self.cache.write() {
892            *g = SourceCache::default();
893        }
894    }
895}
896
897#[derive(Debug)]
898pub struct EvalResult {
899    pub computed_vertices: usize,
900    pub cycle_errors: usize,
901    pub elapsed: std::time::Duration,
902}
903
904/// Read-only engine counters used by benchmark/instrumentation tooling.
905///
906/// These counters are deliberately observational: collecting them must not mutate engine state or
907/// alter formula evaluation semantics.
908#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
909pub struct EngineBaselineStats {
910    pub graph_vertex_count: usize,
911    pub graph_formula_vertex_count: usize,
912    pub graph_edge_count: usize,
913    pub dirty_vertex_count: usize,
914    pub evaluation_vertex_count: usize,
915    pub formula_ast_root_count: usize,
916    pub formula_ast_node_count: usize,
917    pub staged_formula_count: usize,
918    pub formula_plane_active_span_count: usize,
919    pub formula_plane_producer_result_entries: usize,
920    pub formula_plane_consumer_read_entries: usize,
921}
922
923#[derive(Debug, Clone, Default)]
924pub struct VirtualDepTelemetry {
925    pub candidate_vertices_total: usize,
926    pub vdeps_vertices_total: usize,
927    pub vdeps_edges_total: usize,
928    pub builder_elapsed_ms_total: u128,
929    pub schedule_virtual_passes: usize,
930    pub schedule_static_passes: usize,
931    pub schedule_cache_hits: usize,
932    pub schedule_cache_misses: usize,
933    pub reused_schedule_vertices_total: usize,
934    pub replan_iterations: usize,
935    pub changed_vdeps_total: usize,
936    pub bailout_reason: Option<&'static str>,
937    pub fallback_mode_activations: u64,
938}
939
940#[derive(Debug, Clone, Copy)]
941struct ScheduleBuildMeta {
942    candidate_vertices: usize,
943    vdeps_vertices: usize,
944    vdeps_edges: usize,
945    builder_elapsed_ms: u128,
946    used_virtual_schedule: bool,
947    schedule_cache_hit: bool,
948    schedule_cache_eligible: bool,
949}
950
951#[derive(Debug, Clone)]
952struct CachedScheduleEntry {
953    topology_epoch: u64,
954    candidate_vertices: Vec<VertexId>,
955    schedule: crate::engine::scheduler::Schedule,
956}
957
958type ScheduleBuildOutput = (
959    crate::engine::scheduler::Schedule,
960    FxHashMap<VertexId, Vec<VertexId>>,
961    ScheduleBuildMeta,
962);
963
964/// Cached evaluation schedule that can be replayed across multiple recalculations.
965#[derive(Debug)]
966pub struct RecalcPlan {
967    schedule: crate::engine::Schedule,
968    has_dynamic_refs: bool,
969}
970
971impl RecalcPlan {
972    pub fn layer_count(&self) -> usize {
973        self.schedule.layers.len()
974    }
975
976    pub fn has_dynamic_refs(&self) -> bool {
977        self.has_dynamic_refs
978    }
979}
980
981#[cfg(test)]
982pub(crate) mod criteria_mask_test_hooks {
983    use std::cell::Cell;
984
985    thread_local! {
986        static TEXT_SEGMENTS_TOTAL: Cell<usize> = const { Cell::new(0) };
987        static TEXT_SEGMENTS_ALL_NULL: Cell<usize> = const { Cell::new(0) };
988    }
989
990    pub fn reset_text_segment_counters() {
991        TEXT_SEGMENTS_TOTAL.with(|c| c.set(0));
992        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(0));
993    }
994
995    pub fn text_segment_counters() -> (usize, usize) {
996        let a = TEXT_SEGMENTS_TOTAL.with(|c| c.get());
997        let b = TEXT_SEGMENTS_ALL_NULL.with(|c| c.get());
998        (a, b)
999    }
1000
1001    pub(crate) fn inc_total() {
1002        TEXT_SEGMENTS_TOTAL.with(|c| c.set(c.get() + 1));
1003    }
1004    pub(crate) fn inc_all_null() {
1005        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(c.get() + 1));
1006    }
1007}
1008
1009#[cfg(test)]
1010pub(crate) mod visibility_mask_test_hooks {
1011    use std::cell::Cell;
1012
1013    thread_local! {
1014        static HITS: Cell<usize> = const { Cell::new(0) };
1015        static MISSES: Cell<usize> = const { Cell::new(0) };
1016        static EVICTIONS: Cell<usize> = const { Cell::new(0) };
1017    }
1018
1019    pub fn reset() {
1020        HITS.with(|c| c.set(0));
1021        MISSES.with(|c| c.set(0));
1022        EVICTIONS.with(|c| c.set(0));
1023    }
1024
1025    pub fn counters() -> (usize, usize, usize) {
1026        let hits = HITS.with(|c| c.get());
1027        let misses = MISSES.with(|c| c.get());
1028        let evictions = EVICTIONS.with(|c| c.get());
1029        (hits, misses, evictions)
1030    }
1031
1032    pub(crate) fn inc_hit() {
1033        HITS.with(|c| c.set(c.get() + 1));
1034    }
1035
1036    pub(crate) fn inc_miss() {
1037        MISSES.with(|c| c.set(c.get() + 1));
1038    }
1039
1040    pub(crate) fn inc_eviction() {
1041        EVICTIONS.with(|c| c.set(c.get() + 1));
1042    }
1043}
1044
1045fn compute_criteria_mask(
1046    view: &RangeView<'_>,
1047    col_in_view: usize,
1048    pred: &crate::args::CriteriaPredicate,
1049) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
1050    use crate::compute_prelude::{boolean, cmp, concat_arrays};
1051    use arrow::compute::kernels::comparison::{ilike, nilike};
1052    use arrow_array::{
1053        Array as _, ArrayRef, BooleanArray, Float64Array, StringArray, builder::BooleanBuilder,
1054    };
1055
1056    // Helper: apply a numeric predicate to a single Float64Array chunk
1057    fn apply_numeric_pred(
1058        chunk: &Float64Array,
1059        pred: &crate::args::CriteriaPredicate,
1060    ) -> Option<BooleanArray> {
1061        match pred {
1062            crate::args::CriteriaPredicate::Gt(n) => {
1063                cmp::gt(chunk, &Float64Array::new_scalar(*n)).ok()
1064            }
1065            crate::args::CriteriaPredicate::Ge(n) => {
1066                cmp::gt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
1067            }
1068            crate::args::CriteriaPredicate::Lt(n) => {
1069                cmp::lt(chunk, &Float64Array::new_scalar(*n)).ok()
1070            }
1071            crate::args::CriteriaPredicate::Le(n) => {
1072                cmp::lt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
1073            }
1074            crate::args::CriteriaPredicate::Eq(v) => match v {
1075                formualizer_common::LiteralValue::Number(x) => {
1076                    cmp::eq(chunk, &Float64Array::new_scalar(*x)).ok()
1077                }
1078                formualizer_common::LiteralValue::Int(i) => {
1079                    cmp::eq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
1080                }
1081                _ => None,
1082            },
1083            crate::args::CriteriaPredicate::Ne(v) => match v {
1084                formualizer_common::LiteralValue::Number(x) => {
1085                    cmp::neq(chunk, &Float64Array::new_scalar(*x)).ok()
1086                }
1087                formualizer_common::LiteralValue::Int(i) => {
1088                    cmp::neq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
1089                }
1090                _ => None,
1091            },
1092            _ => None,
1093        }
1094    }
1095
1096    // Check if this is a numeric predicate that can be applied per-chunk
1097    let is_numeric_pred = matches!(
1098        pred,
1099        crate::args::CriteriaPredicate::Gt(_)
1100            | crate::args::CriteriaPredicate::Ge(_)
1101            | crate::args::CriteriaPredicate::Lt(_)
1102            | crate::args::CriteriaPredicate::Le(_)
1103            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Number(_))
1104            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Int(_))
1105            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Number(_))
1106            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Int(_))
1107    );
1108
1109    // OPTIMIZED PATH: For numeric predicates, apply per-chunk and concatenate boolean masks.
1110    // This avoids materializing the full numeric column (64-bit per element) and instead
1111    // concatenates boolean masks (1-bit per element) - a 64x memory reduction.
1112    if is_numeric_pred {
1113        let mut bool_parts: Vec<BooleanArray> = Vec::new();
1114        for res in view.numbers_slices() {
1115            let (_rs, _rl, cols_seg) = res.ok()?;
1116            if col_in_view < cols_seg.len() {
1117                let chunk = cols_seg[col_in_view].as_ref();
1118                let mask = apply_numeric_pred(chunk, pred)?;
1119                bool_parts.push(mask);
1120            }
1121        }
1122
1123        if bool_parts.is_empty() {
1124            return None;
1125        } else if bool_parts.len() == 1 {
1126            return Some(std::sync::Arc::new(bool_parts.remove(0)));
1127        } else {
1128            // Concatenate boolean masks (much cheaper than concatenating Float64 arrays)
1129            let anys: Vec<&dyn arrow_array::Array> = bool_parts
1130                .iter()
1131                .map(|a| a as &dyn arrow_array::Array)
1132                .collect();
1133            let conc: ArrayRef = concat_arrays(&anys).ok()?;
1134            let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
1135            return Some(std::sync::Arc::new(ba));
1136        }
1137    }
1138
1139    // TEXT PATH: build masks per row-chunk using lowered text slices.
1140    // This avoids concatenating full-string columns just to compute a boolean mask.
1141    let (text_kind, text_pat, empty_special) = match pred {
1142        crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(t)) => {
1143            (0u8, t.to_lowercase(), t.is_empty())
1144        }
1145        crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Text(t)) => {
1146            (1u8, t.to_lowercase(), false)
1147        }
1148        crate::args::CriteriaPredicate::TextLike {
1149            pattern,
1150            case_insensitive,
1151        } => {
1152            let p = if *case_insensitive {
1153                pattern.to_lowercase()
1154            } else {
1155                pattern.clone()
1156            };
1157            (2u8, p.replace('*', "%").replace('?', "_"), false)
1158        }
1159        _ => return None,
1160    };
1161
1162    let pat = StringArray::new_scalar(text_pat);
1163    let mut bool_parts: Vec<BooleanArray> = Vec::new();
1164
1165    for res in view.iter_row_chunks() {
1166        let cs = res.ok()?;
1167        if cs.row_len == 0 {
1168            continue;
1169        }
1170        #[cfg(test)]
1171        criteria_mask_test_hooks::inc_total();
1172
1173        let slices = view.slice_lowered_text(cs.row_start, cs.row_len);
1174        if col_in_view >= slices.len() {
1175            return None;
1176        }
1177
1178        let seg_opt = slices[col_in_view].as_ref().map(|a| a.as_ref());
1179        let seg = match seg_opt {
1180            Some(s) => s,
1181            None => {
1182                #[cfg(test)]
1183                criteria_mask_test_hooks::inc_all_null();
1184                if text_kind == 0 && empty_special {
1185                    // Eq("") treats nulls (Empty) as equal.
1186                    let mut bb = BooleanBuilder::with_capacity(cs.row_len);
1187                    bb.append_n(cs.row_len, true);
1188                    bool_parts.push(bb.finish());
1189                } else {
1190                    // For non-empty patterns, ilike/nilike return null on null inputs.
1191                    bool_parts.push(BooleanArray::new_null(cs.row_len));
1192                }
1193                continue;
1194            }
1195        };
1196
1197        let seg_sa = seg.as_any().downcast_ref::<StringArray>()?;
1198        let mut m = match text_kind {
1199            0 => ilike(seg_sa, &pat).ok()?,
1200            1 => nilike(seg_sa, &pat).ok()?,
1201            2 => ilike(seg_sa, &pat).ok()?,
1202            _ => return None,
1203        };
1204
1205        if text_kind == 0 && empty_special {
1206            // Treat nulls as equal to empty string
1207            let mut bb = BooleanBuilder::with_capacity(seg_sa.len());
1208            for i in 0..seg_sa.len() {
1209                bb.append_value(seg_sa.is_null(i));
1210            }
1211            let nulls = bb.finish();
1212            m = boolean::or_kleene(&m, &nulls).ok()?;
1213        }
1214
1215        bool_parts.push(m);
1216    }
1217
1218    if bool_parts.is_empty() {
1219        None
1220    } else if bool_parts.len() == 1 {
1221        Some(std::sync::Arc::new(bool_parts.remove(0)))
1222    } else {
1223        let anys: Vec<&dyn arrow_array::Array> = bool_parts
1224            .iter()
1225            .map(|a| a as &dyn arrow_array::Array)
1226            .collect();
1227        let conc: ArrayRef = concat_arrays(&anys).ok()?;
1228        let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
1229        Some(std::sync::Arc::new(ba))
1230    }
1231}
1232
1233#[derive(Debug, Clone)]
1234pub struct LayerInfo {
1235    pub vertex_count: usize,
1236    pub parallel_eligible: bool,
1237    pub sample_cells: Vec<String>, // Sample of up to 5 cell addresses
1238}
1239
1240#[derive(Debug, Clone)]
1241pub struct EvalPlan {
1242    pub total_vertices_to_evaluate: usize,
1243    pub layers: Vec<LayerInfo>,
1244    pub cycles_detected: usize,
1245    pub dirty_count: usize,
1246    pub volatile_count: usize,
1247    pub parallel_enabled: bool,
1248    pub estimated_parallel_layers: usize,
1249    pub target_cells: Vec<String>,
1250}
1251
1252impl<R> Engine<R>
1253where
1254    R: EvaluationContext,
1255{
1256    pub fn new(resolver: R, config: EvalConfig) -> Self {
1257        crate::builtins::load_builtins();
1258
1259        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
1260            #[cfg(feature = "system-clock")]
1261            {
1262                Arc::new(crate::timezone::SystemClock::new(
1263                    crate::timezone::TimeZoneSpec::default(),
1264                ))
1265            }
1266            #[cfg(not(feature = "system-clock"))]
1267            {
1268                Arc::new(crate::timezone::FixedClock::new(
1269                    chrono::DateTime::UNIX_EPOCH,
1270                    crate::timezone::TimeZoneSpec::Utc,
1271                ))
1272            }
1273        });
1274
1275        // Initialize thread pool based on config
1276        let thread_pool = if config.enable_parallel {
1277            let mut builder = ThreadPoolBuilder::new();
1278            if let Some(max_threads) = config.max_threads {
1279                builder = builder.num_threads(max_threads);
1280            }
1281
1282            match builder.build() {
1283                Ok(pool) => Some(Arc::new(pool)),
1284                Err(_) => {
1285                    // Fall back to sequential evaluation if thread pool creation fails
1286                    None
1287                }
1288            }
1289        } else {
1290            None
1291        };
1292
1293        let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
1294        let mut engine = Self {
1295            graph: DependencyGraph::new_with_config(config.clone()),
1296            resolver,
1297            config,
1298            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
1299            clock,
1300            thread_pool,
1301            recalc_epoch: 0,
1302            snapshot_id: std::sync::atomic::AtomicU64::new(1),
1303            topology_epoch: 0,
1304            cached_static_schedule: None,
1305            spill_mgr: ShimSpillManager::default(),
1306            arrow_sheets: SheetStore::default(),
1307            has_edited: false,
1308            overlay_compactions: 0,
1309            computed_overlay_bytes_estimate: 0,
1310            computed_overlay_mirroring_disabled: false,
1311            force_materialize_range_views: false,
1312            row_bounds_cache: std::sync::RwLock::new(None),
1313            used_axis_bounds_cache: std::sync::RwLock::new(None),
1314            lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
1315            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
1316            staged_formulas: std::collections::HashMap::new(),
1317            row_visibility: FxHashMap::default(),
1318            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
1319            formula_parse_diagnostics: Vec::new(),
1320            last_formula_ingest_report: None,
1321            formula_ingest_report_total: FormulaIngestReport::default(),
1322            active_cancel_flag: None,
1323            action_depth: 0,
1324            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
1325            virtual_dep_fallback_activations: 0,
1326            formula_plane_indexes_epoch_seen: 0,
1327            #[cfg(test)]
1328            last_formula_plane_span_eval_report: None,
1329        };
1330        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
1331        engine.config.arrow_storage_enabled = true;
1332        engine.config.delta_overlay_enabled = true;
1333        engine.config.write_formula_overlay_enabled = true;
1334        let default_sheet = engine.graph.default_sheet_name().to_string();
1335        engine.ensure_arrow_sheet(&default_sheet);
1336        engine
1337    }
1338
1339    /// Create an Engine with a custom thread pool (for shared thread pool scenarios)
1340    pub fn with_thread_pool(
1341        resolver: R,
1342        config: EvalConfig,
1343        thread_pool: Arc<rayon::ThreadPool>,
1344    ) -> Self {
1345        crate::builtins::load_builtins();
1346        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
1347            #[cfg(feature = "system-clock")]
1348            {
1349                Arc::new(crate::timezone::SystemClock::new(
1350                    crate::timezone::TimeZoneSpec::default(),
1351                ))
1352            }
1353            #[cfg(not(feature = "system-clock"))]
1354            {
1355                Arc::new(crate::timezone::FixedClock::new(
1356                    chrono::DateTime::UNIX_EPOCH,
1357                    crate::timezone::TimeZoneSpec::Utc,
1358                ))
1359            }
1360        });
1361        let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
1362        let mut engine = Self {
1363            graph: DependencyGraph::new_with_config(config.clone()),
1364            resolver,
1365            config,
1366            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
1367            clock,
1368            thread_pool: Some(thread_pool),
1369            recalc_epoch: 0,
1370            snapshot_id: std::sync::atomic::AtomicU64::new(1),
1371            topology_epoch: 0,
1372            cached_static_schedule: None,
1373            spill_mgr: ShimSpillManager::default(),
1374            arrow_sheets: SheetStore::default(),
1375            has_edited: false,
1376            overlay_compactions: 0,
1377            computed_overlay_bytes_estimate: 0,
1378            computed_overlay_mirroring_disabled: false,
1379            force_materialize_range_views: false,
1380            row_bounds_cache: std::sync::RwLock::new(None),
1381            used_axis_bounds_cache: std::sync::RwLock::new(None),
1382            lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
1383            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
1384            staged_formulas: std::collections::HashMap::new(),
1385            row_visibility: FxHashMap::default(),
1386            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
1387            formula_parse_diagnostics: Vec::new(),
1388            last_formula_ingest_report: None,
1389            formula_ingest_report_total: FormulaIngestReport::default(),
1390            active_cancel_flag: None,
1391            action_depth: 0,
1392            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
1393            virtual_dep_fallback_activations: 0,
1394            formula_plane_indexes_epoch_seen: 0,
1395            #[cfg(test)]
1396            last_formula_plane_span_eval_report: None,
1397        };
1398        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
1399        engine.config.arrow_storage_enabled = true;
1400        engine.config.delta_overlay_enabled = true;
1401        engine.config.write_formula_overlay_enabled = true;
1402        let default_sheet = engine.graph.default_sheet_name().to_string();
1403        engine.ensure_arrow_sheet(&default_sheet);
1404        engine
1405    }
1406
1407    pub fn workbook_load_limits(&self) -> &crate::engine::WorkbookLoadLimits {
1408        &self.workbook_load_limits
1409    }
1410
1411    pub fn set_workbook_load_limits(&mut self, limits: crate::engine::WorkbookLoadLimits) {
1412        self.workbook_load_limits = limits;
1413    }
1414
1415    fn clear_source_cache(&self) {
1416        if let Ok(mut g) = self.source_cache.write() {
1417            *g = SourceCache::default();
1418        }
1419    }
1420
1421    pub fn last_virtual_dep_telemetry(&self) -> &VirtualDepTelemetry {
1422        &self.last_virtual_dep_telemetry
1423    }
1424
1425    pub fn virtual_dep_fallback_activations(&self) -> u64 {
1426        self.virtual_dep_fallback_activations
1427    }
1428
1429    pub(crate) fn last_lookup_index_cache_report(&self) -> LookupIndexCacheReport {
1430        self.lookup_index_cache.report()
1431    }
1432
1433    fn lookup_view_contains_volatile(&self, view: &RangeView<'_>, sheet_id: SheetId) -> bool {
1434        let start_row = view.start_row();
1435        let end_row = view.end_row();
1436        let start_col = view.start_col();
1437        let end_col = view.end_col();
1438        for row in start_row..=end_row {
1439            let Ok(row_u32) = u32::try_from(row) else {
1440                return true;
1441            };
1442            for col in start_col..=end_col {
1443                let Ok(col_u32) = u32::try_from(col) else {
1444                    return true;
1445                };
1446                let cell_ref = self
1447                    .graph
1448                    .make_cell_ref_internal(sheet_id, row_u32, col_u32);
1449                if let Some(vertex_id) = self.graph.get_vertex_id_for_address(&cell_ref)
1450                    && self.graph.is_volatile(*vertex_id)
1451                {
1452                    return true;
1453                }
1454            }
1455        }
1456        false
1457    }
1458
1459    fn build_lookup_index_impl(
1460        &self,
1461        view: &RangeView<'_>,
1462        axis: LookupAxis,
1463    ) -> Option<Arc<LookupIndex>> {
1464        let (rows, cols) = view.dims();
1465        if rows == 0 || cols == 0 {
1466            self.lookup_index_cache.note_skipped_tiny();
1467            return None;
1468        }
1469        let len = match axis {
1470            LookupAxis::ColumnInView(col) => {
1471                if col >= cols {
1472                    self.lookup_index_cache.note_skipped_tiny();
1473                    return None;
1474                }
1475                rows
1476            }
1477            LookupAxis::RowInView(row) => {
1478                if row >= rows {
1479                    self.lookup_index_cache.note_skipped_tiny();
1480                    return None;
1481                }
1482                cols
1483            }
1484        };
1485        if len < 64 {
1486            self.lookup_index_cache.note_skipped_tiny();
1487            return None;
1488        }
1489
1490        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
1491        let key = LookupIndexKey {
1492            sheet_id,
1493            start_row: u32::try_from(view.start_row()).ok()?,
1494            start_col: u32::try_from(view.start_col()).ok()?,
1495            end_row: u32::try_from(view.end_row()).ok()?,
1496            end_col: u32::try_from(view.end_col()).ok()?,
1497            axis,
1498            snapshot_id: self.data_snapshot_id(),
1499        };
1500        if let Some(index) = self.lookup_index_cache.get(&key) {
1501            return Some(index);
1502        }
1503        if self
1504            .lookup_index_cache
1505            .would_exceed_cap(estimate_bytes(len, 0))
1506        {
1507            self.lookup_index_cache.note_skipped_cap();
1508            return None;
1509        }
1510        if !self.lookup_index_cache.should_build(key) {
1511            return None;
1512        }
1513        if self.lookup_index_cache.is_known_volatile(&key) {
1514            self.lookup_index_cache.note_skipped_volatile();
1515            return None;
1516        }
1517        if self.lookup_view_contains_volatile(view, sheet_id) {
1518            self.lookup_index_cache.note_volatile_key(key);
1519            self.lookup_index_cache.note_skipped_volatile();
1520            return None;
1521        }
1522        match LookupIndex::build(view, axis).ok()? {
1523            BuildOutcome::Built(index) => self.lookup_index_cache.insert_if_room(key, index),
1524            BuildOutcome::ErrorInLookupAxis => {
1525                self.lookup_index_cache.note_skipped_error();
1526                None
1527            }
1528            BuildOutcome::Degenerate => {
1529                self.lookup_index_cache.note_skipped_tiny();
1530                None
1531            }
1532        }
1533    }
1534
1535    fn reset_virtual_dep_telemetry_if_disabled(&mut self) {
1536        if !self.config.enable_virtual_dep_telemetry {
1537            self.last_virtual_dep_telemetry = VirtualDepTelemetry {
1538                fallback_mode_activations: self.virtual_dep_fallback_activations,
1539                ..VirtualDepTelemetry::default()
1540            };
1541        }
1542    }
1543
1544    fn source_cache_session(&self) -> SourceCacheSession {
1545        self.clear_source_cache();
1546        SourceCacheSession {
1547            cache: self.source_cache.clone(),
1548        }
1549    }
1550
1551    fn resolve_source_scalar_cached(
1552        &self,
1553        name: &str,
1554        version: Option<u64>,
1555    ) -> Result<LiteralValue, ExcelError> {
1556        let key = (name.to_string(), version);
1557        if let Ok(mut g) = self.source_cache.write() {
1558            if let Some(v) = g.scalars.get(&key) {
1559                return Ok(v.clone());
1560            }
1561
1562            let v = self.resolver.resolve_source_scalar(name).map_err(|err| {
1563                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
1564                    ExcelError::new(ExcelErrorKind::Ref)
1565                        .with_message(format!("Unresolved source scalar: {name}"))
1566                } else {
1567                    err
1568                }
1569            })?;
1570            g.scalars.insert(key, v.clone());
1571            Ok(v)
1572        } else {
1573            self.resolver.resolve_source_scalar(name).map_err(|err| {
1574                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
1575                    ExcelError::new(ExcelErrorKind::Ref)
1576                        .with_message(format!("Unresolved source scalar: {name}"))
1577                } else {
1578                    err
1579                }
1580            })
1581        }
1582    }
1583
1584    fn resolve_source_table_cached(
1585        &self,
1586        name: &str,
1587        version: Option<u64>,
1588    ) -> Result<Arc<dyn crate::traits::Table>, ExcelError> {
1589        let key = (name.to_string(), version);
1590        if let Ok(mut g) = self.source_cache.write() {
1591            if let Some(t) = g.tables.get(&key) {
1592                return Ok(t.clone());
1593            }
1594
1595            let t = self.resolver.resolve_source_table(name).map_err(|err| {
1596                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
1597                    ExcelError::new(ExcelErrorKind::Ref)
1598                        .with_message(format!("Unresolved source table: {name}"))
1599                } else {
1600                    err
1601                }
1602            })?;
1603            let t: Arc<dyn crate::traits::Table> = Arc::from(t);
1604            g.tables.insert(key, t.clone());
1605            Ok(t)
1606        } else {
1607            self.resolver
1608                .resolve_source_table(name)
1609                .map_err(|err| {
1610                    if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
1611                        ExcelError::new(ExcelErrorKind::Ref)
1612                            .with_message(format!("Unresolved source table: {name}"))
1613                    } else {
1614                        err
1615                    }
1616                })
1617                .map(Arc::from)
1618        }
1619    }
1620
1621    fn source_table_to_range_view(
1622        &self,
1623        table: &dyn crate::traits::Table,
1624        spec: &Option<formualizer_parse::parser::TableSpecifier>,
1625    ) -> Result<RangeView<'static>, ExcelError> {
1626        use formualizer_parse::parser::{SpecialItem, TableSpecifier};
1627
1628        let owned = match spec {
1629            Some(TableSpecifier::Column(c)) => {
1630                let c = c.trim();
1631                if c == "@" || c.contains('[') || c.contains(']') || c.contains(',') {
1632                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
1633                        "Complex structured references not yet supported".to_string(),
1634                    ));
1635                }
1636                table.get_column(c)?.materialise().into_owned()
1637            }
1638            Some(TableSpecifier::ColumnRange(start, end)) => {
1639                let cols = table.columns();
1640                let start = start.trim();
1641                let end = end.trim();
1642                let start_key = start.to_lowercase();
1643                let end_key = end.to_lowercase();
1644                let start_idx = cols.iter().position(|n| n.to_lowercase() == start_key);
1645                let end_idx = cols.iter().position(|n| n.to_lowercase() == end_key);
1646                if let (Some(mut si), Some(mut ei)) = (start_idx, end_idx) {
1647                    if si > ei {
1648                        std::mem::swap(&mut si, &mut ei);
1649                    }
1650                    let h = table.data_height();
1651                    let w = ei - si + 1;
1652                    let mut rows = vec![vec![LiteralValue::Empty; w]; h];
1653                    for (offset, ci) in (si..=ei).enumerate() {
1654                        let cname = &cols[ci];
1655                        let col_range = table.get_column(cname)?;
1656                        let (rh, _) = col_range.dimensions();
1657                        for (r, row) in rows.iter_mut().enumerate().take(h.min(rh)) {
1658                            row[offset] = col_range.get(r, 0)?;
1659                        }
1660                    }
1661                    rows
1662                } else {
1663                    return Err(ExcelError::new(ExcelErrorKind::Ref)
1664                        .with_message("Column range refers to unknown column(s)".to_string()));
1665                }
1666            }
1667            Some(TableSpecifier::SpecialItem(SpecialItem::Headers))
1668            | Some(TableSpecifier::Headers) => table
1669                .headers_row()
1670                .map(|r| r.materialise().into_owned())
1671                .unwrap_or_default(),
1672            Some(TableSpecifier::SpecialItem(SpecialItem::Totals))
1673            | Some(TableSpecifier::Totals) => table
1674                .totals_row()
1675                .map(|r| r.materialise().into_owned())
1676                .unwrap_or_default(),
1677            Some(TableSpecifier::SpecialItem(SpecialItem::Data)) | Some(TableSpecifier::Data) => {
1678                table
1679                    .data_body()
1680                    .map(|r| r.materialise().into_owned())
1681                    .unwrap_or_default()
1682            }
1683            Some(TableSpecifier::SpecialItem(SpecialItem::All)) | Some(TableSpecifier::All) => {
1684                let mut out: Vec<Vec<LiteralValue>> = Vec::new();
1685                if let Some(h) = table.headers_row() {
1686                    out.extend(h.iter_rows());
1687                }
1688                if let Some(body) = table.data_body() {
1689                    out.extend(body.iter_rows());
1690                }
1691                if let Some(tr) = table.totals_row() {
1692                    out.extend(tr.iter_rows());
1693                }
1694                out
1695            }
1696            Some(TableSpecifier::SpecialItem(SpecialItem::ThisRow)) => {
1697                return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
1698                    "@ (This Row) requires table-aware context; not yet supported".to_string(),
1699                ));
1700            }
1701            Some(TableSpecifier::Row(_)) | Some(TableSpecifier::Combination(_)) => {
1702                return Err(ExcelError::new(ExcelErrorKind::NImpl)
1703                    .with_message("Complex structured references not yet supported".to_string()));
1704            }
1705            None => {
1706                return Err(ExcelError::new(ExcelErrorKind::NImpl)
1707                    .with_message("Table reference without specifier is unsupported".to_string()));
1708            }
1709        };
1710
1711        Ok(RangeView::from_owned_rows(owned, self.config.date_system))
1712    }
1713
1714    pub fn default_sheet_id(&self) -> SheetId {
1715        self.graph.default_sheet_id()
1716    }
1717
1718    pub fn default_sheet_name(&self) -> &str {
1719        self.graph.default_sheet_name()
1720    }
1721
1722    /// Update the workbook seed for deterministic RNGs in functions.
1723    pub fn set_workbook_seed(&mut self, seed: u64) {
1724        self.config.workbook_seed = seed;
1725    }
1726
1727    /// Set the volatile level policy (Always/OnRecalc/OnOpen)
1728    pub fn set_volatile_level(&mut self, level: crate::traits::VolatileLevel) {
1729        self.config.volatile_level = level;
1730    }
1731
1732    /// Enable/disable deterministic evaluation mode (fixed clock + timezone).
1733    pub fn set_deterministic_mode(
1734        &mut self,
1735        mode: crate::engine::DeterministicMode,
1736    ) -> Result<(), ExcelError> {
1737        let clock = mode.build_clock()?;
1738        self.config.deterministic_mode = mode;
1739        self.clock = clock;
1740        Ok(())
1741    }
1742
1743    fn validate_deterministic_mode(&self) -> Result<(), ExcelError> {
1744        self.config.deterministic_mode.validate()
1745    }
1746
1747    pub fn sheet_id(&self, name: &str) -> Option<SheetId> {
1748        self.graph.sheet_id(name)
1749    }
1750
1751    pub fn sheet_id_mut(&mut self, name: &str) -> SheetId {
1752        self.add_sheet(name)
1753            .unwrap_or_else(|_| self.graph.sheet_id_mut(name))
1754    }
1755
1756    pub fn sheet_name(&self, id: SheetId) -> &str {
1757        self.graph.sheet_name(id)
1758    }
1759
1760    pub fn add_sheet(&mut self, name: &str) -> Result<SheetId, ExcelError> {
1761        let id = self.graph.add_sheet(name)?;
1762        self.ensure_arrow_sheet(name);
1763        // Adding a sheet does not invalidate existing SheetId-based FormulaPlane
1764        // spans. `graph.add_sheet` handles legacy orphan-healing for formulas
1765        // that were explicitly tombstoned for this sheet name; avoid a global
1766        // FormulaPlane demotion/dirty mark for unrelated spans.
1767        self.mark_topology_edited();
1768        Ok(id)
1769    }
1770
1771    pub fn duplicate_sheet(&mut self, source: &str, new_name: &str) -> Result<SheetId, ExcelError> {
1772        let source_id = self.graph.sheet_id(source).ok_or_else(|| {
1773            ExcelError::new(ExcelErrorKind::Value).with_message("Source sheet does not exist")
1774        })?;
1775        // Materialize only spans on the source sheet so graph duplication sees
1776        // the formulas being copied. Spans on unrelated sheets remain active.
1777        self.demote_spans_preserving_computed_overlays(source_id, Region::whole_sheet(source_id))
1778            .map_err(Self::editor_error_to_excel)?;
1779        let new_id = self.graph.duplicate_sheet(source_id, new_name)?;
1780
1781        if let Some(source_sheet) = self.arrow_sheets.sheet(source).cloned() {
1782            let mut copied_sheet = source_sheet;
1783            copied_sheet.name = Arc::<str>::from(new_name);
1784            self.arrow_sheets.sheets.push(copied_sheet);
1785        } else {
1786            self.ensure_arrow_sheet(new_name);
1787        }
1788
1789        self.clear_all_computed_overlays();
1790        self.mark_all_formula_vertices_dirty();
1791        self.mark_topology_edited();
1792        Ok(new_id)
1793    }
1794
1795    fn ensure_arrow_sheet(&mut self, name: &str) {
1796        if self.arrow_sheets.sheet(name).is_some() {
1797            return;
1798        }
1799        self.arrow_sheets
1800            .sheets
1801            .push(crate::arrow_store::ArrowSheet {
1802                name: std::sync::Arc::<str>::from(name),
1803                columns: Vec::new(),
1804                nrows: 0,
1805                chunk_starts: Vec::new(),
1806                chunk_rows: 32 * 1024,
1807            });
1808    }
1809
1810    pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
1811        let name = self.graph.sheet_name(sheet_id).to_string();
1812        // Removing a sheet only affects spans on that sheet and spans reading
1813        // from that sheet. Preserve spans on unrelated sheets so sheet
1814        // lifecycle operations do not collapse the whole FormulaPlane.
1815        self.demote_spans_preserving_computed_overlays(sheet_id, Region::whole_sheet(sheet_id))
1816            .map_err(Self::editor_error_to_excel)?;
1817        self.graph.remove_sheet(sheet_id)?;
1818        self.arrow_sheets.sheets.retain(|s| s.name.as_ref() != name);
1819        self.clear_all_computed_overlays();
1820        self.mark_all_formula_vertices_dirty();
1821        self.staged_formulas.remove(&name);
1822        if self.row_visibility.remove(&sheet_id).is_some() {
1823            self.invalidate_row_visibility_mask_cache();
1824        }
1825        self.record_formula_plane_structural_change(StructuralScope::RemovedSheet(sheet_id));
1826        self.mark_topology_edited();
1827        Ok(())
1828    }
1829
1830    /// Helper to synchronize the Arrow-backed storage layer.
1831    fn rename_sheet_in_arrow_store(&mut self, target_name: &str, new_name: &str) -> bool {
1832        if let Some(asheet) = self
1833            .arrow_sheets
1834            .sheets
1835            .iter_mut()
1836            .find(|s| s.name.as_ref() == target_name)
1837        {
1838            asheet.name = std::sync::Arc::<str>::from(new_name);
1839            return true;
1840        }
1841        false
1842    }
1843
1844    pub fn rename_sheet(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
1845        let old_name = self.graph.sheet_name(sheet_id).to_string();
1846
1847        // Speculative Storage Update
1848        // Update name in storage FIRST so the Evaluator can find it during Graph rescue.
1849        self.rename_sheet_in_arrow_store(&old_name, new_name);
1850
1851        // Graph Update (Metadata + Rescue Logic)
1852        match self.graph.rename_sheet(sheet_id, new_name) {
1853            Ok(_) => {
1854                self.rename_staged_formula_sheet(&old_name, new_name);
1855                // Success! Invalidate cache for the moved sheet
1856                let sheet_vertices: Vec<VertexId> =
1857                    self.graph.vertices_in_sheet(sheet_id).collect();
1858                for v_id in sheet_vertices {
1859                    self.graph.mark_vertex_dirty(v_id);
1860                }
1861                // Sheet rename is metadata-only and preserves SheetId. References resolve by
1862                // SheetId, so no FormulaPlane changed region is required. Removing this avoids
1863                // re-evaluating every span that reads the renamed sheet.
1864                self.mark_topology_edited();
1865                Ok(())
1866            }
1867            Err(e) => {
1868                // ROLLBACK: Revert storage if graph rejected the name
1869                self.rename_sheet_in_arrow_store(new_name, &old_name);
1870                Err(e)
1871            }
1872        }
1873    }
1874
1875    pub fn named_ranges_iter(
1876        &self,
1877    ) -> impl Iterator<Item = (&String, &crate::engine::named_range::NamedRange)> {
1878        self.graph.named_ranges_iter()
1879    }
1880
1881    pub fn sheet_named_ranges_iter(
1882        &self,
1883    ) -> impl Iterator<Item = (&(SheetId, String), &crate::engine::named_range::NamedRange)> {
1884        self.graph.sheet_named_ranges_iter()
1885    }
1886
1887    pub fn resolve_name_entry(
1888        &self,
1889        name: &str,
1890        current_sheet: SheetId,
1891    ) -> Option<&crate::engine::named_range::NamedRange> {
1892        self.graph.resolve_name_entry(name, current_sheet)
1893    }
1894
1895    pub fn named_ranges_snapshot(&self) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
1896        let mut out: Vec<crate::engine::named_range::NamedRangeSnapshot> = Vec::new();
1897
1898        for (name, named) in self.graph.named_ranges_iter() {
1899            out.push(crate::engine::named_range::NamedRangeSnapshot {
1900                name: name.clone(),
1901                scope: NameScope::Workbook,
1902                definition: named.definition.clone(),
1903            });
1904        }
1905
1906        for ((sheet_id, name), named) in self.graph.sheet_named_ranges_iter() {
1907            out.push(crate::engine::named_range::NamedRangeSnapshot {
1908                name: name.clone(),
1909                scope: NameScope::Sheet(*sheet_id),
1910                definition: named.definition.clone(),
1911            });
1912        }
1913
1914        out.sort_by(|a, b| {
1915            let a_scope = match a.scope {
1916                NameScope::Workbook => (0u8, 0u32),
1917                NameScope::Sheet(id) => (1u8, u32::from(id)),
1918            };
1919            let b_scope = match b.scope {
1920                NameScope::Workbook => (0u8, 0u32),
1921                NameScope::Sheet(id) => (1u8, u32::from(id)),
1922            };
1923            a_scope.cmp(&b_scope).then_with(|| a.name.cmp(&b.name))
1924        });
1925
1926        out
1927    }
1928
1929    pub fn named_ranges_snapshot_for_sheet(
1930        &self,
1931        sheet_id: SheetId,
1932    ) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
1933        self.named_ranges_snapshot()
1934            .into_iter()
1935            .filter(|entry| match entry.scope {
1936                NameScope::Workbook => true,
1937                NameScope::Sheet(id) => id == sheet_id,
1938            })
1939            .collect()
1940    }
1941
1942    pub fn define_name(
1943        &mut self,
1944        name: &str,
1945        definition: NamedDefinition,
1946        scope: NameScope,
1947    ) -> Result<(), ExcelError> {
1948        self.graph.define_name(name, definition, scope)?;
1949        self.record_formula_plane_structural_change(StructuralScope::AllSheets);
1950        self.mark_topology_edited();
1951        Ok(())
1952    }
1953
1954    pub fn update_name(
1955        &mut self,
1956        name: &str,
1957        definition: NamedDefinition,
1958        scope: NameScope,
1959    ) -> Result<(), ExcelError> {
1960        self.graph.update_name(name, definition, scope)?;
1961        self.record_formula_plane_structural_change(StructuralScope::AllSheets);
1962        self.mark_topology_edited();
1963        Ok(())
1964    }
1965
1966    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
1967        self.graph.delete_name(name, scope)?;
1968        self.record_formula_plane_structural_change(StructuralScope::AllSheets);
1969        self.mark_topology_edited();
1970        Ok(())
1971    }
1972
1973    pub fn define_table(
1974        &mut self,
1975        name: &str,
1976        range: crate::reference::RangeRef,
1977        header_row: bool,
1978        headers: Vec<String>,
1979        totals_row: bool,
1980    ) -> Result<(), ExcelError> {
1981        self.graph
1982            .define_table(name, range, header_row, headers, totals_row)?;
1983        self.record_formula_plane_structural_change(StructuralScope::AllSheets);
1984        self.mark_topology_edited();
1985        Ok(())
1986    }
1987
1988    pub fn define_source_scalar(
1989        &mut self,
1990        name: &str,
1991        version: Option<u64>,
1992    ) -> Result<(), ExcelError> {
1993        self.graph.define_source_scalar(name, version)?;
1994        self.record_formula_plane_structural_change(StructuralScope::AllSheets);
1995        self.mark_topology_edited();
1996        Ok(())
1997    }
1998
1999    pub fn define_source_table(
2000        &mut self,
2001        name: &str,
2002        version: Option<u64>,
2003    ) -> Result<(), ExcelError> {
2004        self.graph.define_source_table(name, version)?;
2005        self.record_formula_plane_structural_change(StructuralScope::AllSheets);
2006        self.mark_topology_edited();
2007        Ok(())
2008    }
2009
2010    pub fn set_source_scalar_version(
2011        &mut self,
2012        name: &str,
2013        version: Option<u64>,
2014    ) -> Result<(), ExcelError> {
2015        self.graph.set_source_scalar_version(name, version)?;
2016        self.record_formula_plane_structural_change(StructuralScope::AllSheets);
2017        Ok(())
2018    }
2019
2020    pub fn set_source_table_version(
2021        &mut self,
2022        name: &str,
2023        version: Option<u64>,
2024    ) -> Result<(), ExcelError> {
2025        self.graph.set_source_table_version(name, version)?;
2026        self.record_formula_plane_structural_change(StructuralScope::AllSheets);
2027        Ok(())
2028    }
2029
2030    pub fn invalidate_source(&mut self, name: &str) -> Result<(), ExcelError> {
2031        self.graph.invalidate_source(name)?;
2032        self.record_formula_plane_structural_change(StructuralScope::AllSheets);
2033        Ok(())
2034    }
2035
2036    pub fn vertex_value(&self, vertex: VertexId) -> Option<LiteralValue> {
2037        self.graph.get_value(vertex)
2038    }
2039
2040    pub fn graph_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
2041        self.graph.get_cell_value(sheet, row, col)
2042    }
2043
2044    pub fn vertex_for_cell(&self, cell: &CellRef) -> Option<VertexId> {
2045        self.graph.get_vertex_for_cell(cell)
2046    }
2047
2048    pub fn evaluation_vertices(&self) -> Vec<VertexId> {
2049        self.graph.get_evaluation_vertices()
2050    }
2051
2052    /// Return read-only baseline counters for FormulaPlane/dispatch benchmarking.
2053    pub fn baseline_stats(&self) -> EngineBaselineStats {
2054        let graph = self.graph.baseline_stats();
2055        let formula_authority = self.graph.formula_authority();
2056        EngineBaselineStats {
2057            graph_vertex_count: graph.graph_vertex_count,
2058            graph_formula_vertex_count: graph.graph_formula_vertex_count,
2059            graph_edge_count: graph.graph_edge_count,
2060            dirty_vertex_count: graph.dirty_vertex_count,
2061            evaluation_vertex_count: graph.evaluation_vertex_count,
2062            formula_ast_root_count: graph.formula_ast_root_count,
2063            formula_ast_node_count: graph.formula_ast_node_count,
2064            staged_formula_count: self.staged_formula_count(),
2065            formula_plane_active_span_count: formula_authority.active_span_count(),
2066            formula_plane_producer_result_entries: formula_authority.producer_results.len(),
2067            formula_plane_consumer_read_entries: formula_authority.consumer_reads.len(),
2068        }
2069    }
2070
2071    #[cfg(test)]
2072    pub(crate) fn used_axis_bounds_cache_stats(&self) -> (usize, usize, usize, usize) {
2073        self.used_axis_bounds_cache
2074            .read()
2075            .ok()
2076            .and_then(|guard| {
2077                guard.as_ref().map(|cache| {
2078                    (
2079                        cache.row_hits.load(Ordering::Relaxed),
2080                        cache.row_misses.load(Ordering::Relaxed),
2081                        cache.col_hits.load(Ordering::Relaxed),
2082                        cache.col_misses.load(Ordering::Relaxed),
2083                    )
2084                })
2085            })
2086            .unwrap_or((0, 0, 0, 0))
2087    }
2088
2089    pub fn set_first_load_assume_new(&mut self, enabled: bool) {
2090        self.graph.set_first_load_assume_new(enabled);
2091    }
2092
2093    pub fn reset_ensure_touched(&mut self) {
2094        self.graph.reset_ensure_touched();
2095    }
2096
2097    pub fn finalize_sheet_index(&mut self, sheet: &str) {
2098        self.graph.finalize_sheet_index(sheet);
2099    }
2100
2101    /// Execute a named Engine action.
2102    ///
2103    /// Ticket 614 introduces this as the stable Engine-level transaction surface.
2104    /// For now actions are commit-only: they do not create changelog boundaries and they do not
2105    /// provide rollback/atomicity.
2106    ///
2107    /// Nested actions are deterministically handled by *disallowing* nesting: calling
2108    /// `Engine::action` while another action is active returns `EditorError::TransactionFailed`.
2109    pub fn action<T>(
2110        &mut self,
2111        name: impl AsRef<str>,
2112        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
2113    ) -> Result<T, crate::engine::EditorError> {
2114        if self.action_depth != 0 {
2115            return Err(crate::engine::EditorError::TransactionFailed {
2116                reason: "Nested Engine::action calls are not supported (ticket 614: commit-only surface)"
2117                    .to_string(),
2118            });
2119        }
2120
2121        self.action_depth = 1;
2122        let engine_ptr: *mut Engine<R> = self;
2123        let _guard = ActionDepthGuard {
2124            engine: engine_ptr,
2125            _marker: std::marker::PhantomData,
2126        };
2127
2128        let mut tx = EngineAction {
2129            engine: self,
2130            name: name.as_ref().to_string(),
2131            log: None,
2132            arrow_undo: None,
2133            atomic_policy: false,
2134        };
2135        f(&mut tx)
2136    }
2137
2138    /// Execute a named Engine action with atomic commit/rollback semantics.
2139    ///
2140    /// This variant does not require a `ChangeLog` and uses an internal journal for rollback.
2141    pub fn action_atomic<T>(
2142        &mut self,
2143        name: impl Into<String>,
2144        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
2145    ) -> Result<T, crate::engine::EditorError> {
2146        let (v, _j) = self.action_atomic_journal(name, f)?;
2147        Ok(v)
2148    }
2149
2150    /// Like `action_atomic`, but returns the committed journal entry for undo/redo storage.
2151    pub fn action_atomic_journal<T>(
2152        &mut self,
2153        name: impl Into<String>,
2154        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
2155    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
2156        if self.action_depth != 0 {
2157            return Err(crate::engine::EditorError::TransactionFailed {
2158                reason: "Nested Engine::action calls are not supported (deterministic rule)"
2159                    .to_string(),
2160            });
2161        }
2162
2163        self.action_depth = 1;
2164        let engine_ptr: *mut Engine<R> = self;
2165        let _guard = ActionDepthGuard {
2166            engine: engine_ptr,
2167            _marker: std::marker::PhantomData,
2168        };
2169
2170        let name_str = name.into();
2171        let mut log = crate::engine::ChangeLog::new();
2172        let start_len = log.len();
2173        self.action_atomic_impl(&mut log, start_len, name_str, f)
2174    }
2175
2176    fn action_atomic_impl<T>(
2177        &mut self,
2178        log: &mut crate::engine::ChangeLog,
2179        start_len: usize,
2180        name: String,
2181        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
2182    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
2183        let mut arrow_undo = crate::engine::ArrowUndoBatch::default();
2184        let arrow_ptr: *mut crate::engine::ArrowUndoBatch = &mut arrow_undo;
2185
2186        let log_ptr: *mut crate::engine::ChangeLog = log;
2187        let mut tx = EngineAction {
2188            engine: self,
2189            name: name.clone(),
2190            log: Some(log_ptr),
2191            arrow_undo: Some(arrow_ptr),
2192            atomic_policy: true,
2193        };
2194
2195        let res = f(&mut tx);
2196
2197        // Capture graph structural delta for this action.
2198        let graph_events: Vec<crate::engine::ChangeEvent> =
2199            unsafe { (&*log_ptr).events() }[start_len..].to_vec();
2200        let graph_batch = crate::engine::GraphUndoBatch {
2201            events: graph_events,
2202        };
2203        let affected_cells = arrow_undo.ops.len();
2204        let journal = crate::engine::ActionJournal {
2205            name,
2206            graph: graph_batch,
2207            arrow: arrow_undo,
2208            affected_cells,
2209        };
2210
2211        match res {
2212            Ok(v) => {
2213                if !journal.graph.is_empty() || !journal.arrow.is_empty() {
2214                    for event in &journal.graph.events {
2215                        self.record_formula_plane_change_for_event(event);
2216                    }
2217                    self.mark_data_edited();
2218                }
2219                Ok((v, journal))
2220            }
2221            Err(e) => {
2222                if let Err(rb) = self.rollback_from_action_journal(&journal) {
2223                    return Err(crate::engine::EditorError::TransactionFailed {
2224                        reason: format!(
2225                            "Engine::action_atomic rollback failed after error '{e}': {rb}"
2226                        ),
2227                    });
2228                }
2229                if !journal.graph.is_empty() || !journal.arrow.is_empty() {
2230                    for event in &journal.graph.events {
2231                        self.record_formula_plane_change_for_event(event);
2232                    }
2233                }
2234                Err(e)
2235            }
2236        }
2237    }
2238
2239    /// Execute a named Engine action, logging graph changes into the provided ChangeLog.
2240    ///
2241    /// Ticket 615: this variant provides atomicity. If the action returns an error, it rolls back:
2242    /// - Dependency graph structural edits (via inverse ChangeEvents)
2243    /// - Arrow-truth overlay writes mirrored from ChangeEvents
2244    /// - ChangeLog entries (truncated back to the pre-action length)
2245    pub fn action_with_logger<T>(
2246        &mut self,
2247        log: &mut crate::engine::ChangeLog,
2248        name: impl AsRef<str>,
2249        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
2250    ) -> Result<T, crate::engine::EditorError> {
2251        if self.action_depth != 0 {
2252            return Err(crate::engine::EditorError::TransactionFailed {
2253                reason: "Nested Engine::action calls are not supported (deterministic rule)"
2254                    .to_string(),
2255            });
2256        }
2257
2258        self.action_depth = 1;
2259        let engine_ptr: *mut Engine<R> = self;
2260        let _guard = ActionDepthGuard {
2261            engine: engine_ptr,
2262            _marker: std::marker::PhantomData,
2263        };
2264
2265        let start_len = log.len();
2266        let name_str = name.as_ref().to_string();
2267        log.begin_compound(name_str.clone());
2268
2269        // Use the provided ChangeLog as an observability sink.
2270        // Correctness is provided by the internal `ActionJournal` returned from the atomic impl.
2271        let res = self.action_atomic_impl(log, start_len, name_str, f);
2272
2273        match res {
2274            Ok((v, _journal)) => {
2275                log.end_compound();
2276                Ok(v)
2277            }
2278            Err(e) => {
2279                // Close compound and truncate log as cleanup only.
2280                log.end_compound();
2281                log.truncate(start_len);
2282                Err(e)
2283            }
2284        }
2285    }
2286
2287    fn rollback_from_action_journal(
2288        &mut self,
2289        journal: &crate::engine::ActionJournal,
2290    ) -> Result<(), crate::engine::EditorError> {
2291        // 1) Roll back the dependency graph structure.
2292        journal.graph.undo(&mut self.graph)?;
2293        // 2) Roll back engine row-visibility sidecar events.
2294        self.apply_inverse_row_visibility_events(&journal.graph.events);
2295        // 3) Roll back Arrow-truth overlays.
2296        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
2297        Ok(())
2298    }
2299
2300    fn rollback_from_change_events(
2301        &mut self,
2302        events: &[crate::engine::ChangeEvent],
2303    ) -> Result<(), crate::engine::EditorError> {
2304        use crate::engine::ChangeEvent;
2305
2306        // 1) Roll back the dependency graph.
2307        {
2308            let mut editor = crate::engine::VertexEditor::new(&mut self.graph);
2309            let mut compound_stack: Vec<usize> = Vec::new();
2310            for ev in events.iter().rev() {
2311                match ev {
2312                    ChangeEvent::CompoundEnd { depth } => compound_stack.push(*depth),
2313                    ChangeEvent::CompoundStart { depth, .. } => {
2314                        if compound_stack.last() == Some(depth) {
2315                            compound_stack.pop();
2316                        }
2317                    }
2318                    ChangeEvent::SetRowVisibility { .. } => {
2319                        // Engine-side metadata handled after dropping graph editor borrow.
2320                    }
2321                    _ => {
2322                        editor.apply_inverse(ev.clone())?;
2323                    }
2324                }
2325            }
2326        }
2327
2328        // 2) Roll back engine row-visibility metadata.
2329        for ev in events.iter().rev() {
2330            self.apply_inverse_row_visibility_event(ev);
2331        }
2332
2333        // 3) Roll back Arrow-truth overlays mirrored from those ChangeEvents.
2334        for ev in events.iter().rev() {
2335            self.mirror_inverse_change_to_arrow(ev);
2336        }
2337
2338        Ok(())
2339    }
2340
2341    fn read_cell_formula_ast(&self, sheet: &str, row: u32, col: u32) -> Option<ASTNode> {
2342        let sheet_id = self.graph.sheet_id(sheet)?;
2343        let coord = Coord::from_excel(row, col, true, true);
2344        let cell = CellRef::new(sheet_id, coord);
2345        let vid = self.graph.get_vertex_for_cell(&cell)?;
2346        let ast_id = self.graph.get_formula_id(vid)?;
2347        self.graph
2348            .data_store()
2349            .retrieve_ast(ast_id, self.graph.sheet_reg())
2350    }
2351
2352    pub fn edit_with_logger<T>(
2353        &mut self,
2354        log: &mut crate::engine::ChangeLog,
2355        f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
2356    ) -> T {
2357        // Record starting log length so we can mirror only newly-recorded events.
2358        let start_len = log.len();
2359
2360        // Provide a spill snapshot reader so VertexEditor can snapshot Arrow-truth spill values
2361        // (graph value cache is intentionally empty in canonical mode).
2362        struct ArrowSpillReader<'a> {
2363            sheets: &'a crate::arrow_store::SheetStore,
2364        }
2365        impl crate::engine::graph::editor::vertex_editor::SpillValueReader for ArrowSpillReader<'_> {
2366            fn read_cell_value(
2367                &self,
2368                sheet: &str,
2369                row: u32,
2370                col: u32,
2371            ) -> Option<formualizer_common::LiteralValue> {
2372                use formualizer_common::LiteralValue;
2373                let asheet = self.sheets.sheet(sheet)?;
2374                let r0 = row.saturating_sub(1) as usize;
2375                let c0 = col.saturating_sub(1) as usize;
2376                let v = asheet.get_cell_value(r0, c0);
2377                if matches!(v, LiteralValue::Empty) {
2378                    None
2379                } else {
2380                    Some(v)
2381                }
2382            }
2383        }
2384
2385        let ret = {
2386            let spill_reader = ArrowSpillReader {
2387                sheets: &self.arrow_sheets,
2388            };
2389            let mut editor = crate::engine::VertexEditor::with_logger_and_spill_reader(
2390                &mut self.graph,
2391                log,
2392                &spill_reader,
2393            );
2394            f(&mut editor)
2395        };
2396
2397        // Mirror value-impacting graph events to Arrow for forward edits.
2398        // This keeps Arrow overlays (delta + computed) consistent when edits clear/commit spills.
2399        for ev in &log.events()[start_len..] {
2400            self.mirror_forward_change_to_arrow(ev);
2401        }
2402        for ev in &log.events()[start_len..] {
2403            self.record_formula_plane_change_for_event(ev);
2404        }
2405
2406        ret
2407    }
2408
2409    pub fn undo_logged(
2410        &mut self,
2411        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
2412        log: &mut crate::engine::ChangeLog,
2413    ) -> Result<(), crate::engine::EditorError> {
2414        let batch = undo.undo(&mut self.graph, log)?;
2415        for item in batch.iter().rev() {
2416            self.apply_inverse_row_visibility_event(&item.event);
2417            self.apply_inverse_staged_formula_event(&item.event);
2418        }
2419        self.mirror_undo_batch_to_arrow(&batch);
2420        if !batch.is_empty() {
2421            for item in &batch {
2422                self.record_formula_plane_change_for_event(&item.event);
2423            }
2424        }
2425        Ok(())
2426    }
2427
2428    pub fn redo_logged(
2429        &mut self,
2430        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
2431        log: &mut crate::engine::ChangeLog,
2432    ) -> Result<(), crate::engine::EditorError> {
2433        let batch = undo.redo(&mut self.graph, log)?;
2434        for item in &batch {
2435            self.apply_forward_row_visibility_event(&item.event);
2436            self.apply_forward_staged_formula_event(&item.event);
2437        }
2438        self.mirror_redo_batch_to_arrow(&batch);
2439        if !batch.is_empty() {
2440            for item in &batch {
2441                self.record_formula_plane_change_for_event(&item.event);
2442            }
2443        }
2444        Ok(())
2445    }
2446
2447    /// Undo the last committed atomic action using the journal stack.
2448    ///
2449    /// This path does not require a `ChangeLog`.
2450    pub fn undo_action(
2451        &mut self,
2452        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
2453    ) -> Result<(), crate::engine::EditorError> {
2454        let Some(journal) = undo.pop_undo_action() else {
2455            return Ok(());
2456        };
2457
2458        journal.graph.undo(&mut self.graph)?;
2459        self.apply_inverse_row_visibility_events(&journal.graph.events);
2460        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
2461        if !journal.graph.is_empty() || !journal.arrow.is_empty() {
2462            for event in &journal.graph.events {
2463                self.record_formula_plane_change_for_event(event);
2464            }
2465            self.mark_data_edited();
2466        }
2467
2468        undo.push_redo_action(journal);
2469        Ok(())
2470    }
2471
2472    /// Redo the last undone atomic action using the journal stack.
2473    ///
2474    /// This path does not require a `ChangeLog`.
2475    pub fn redo_action(
2476        &mut self,
2477        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
2478    ) -> Result<(), crate::engine::EditorError> {
2479        let Some(journal) = undo.pop_redo_action() else {
2480            return Ok(());
2481        };
2482
2483        journal.graph.redo(&mut self.graph)?;
2484        self.apply_forward_row_visibility_events(&journal.graph.events);
2485        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ false);
2486        if !journal.graph.is_empty() || !journal.arrow.is_empty() {
2487            for event in &journal.graph.events {
2488                self.record_formula_plane_change_for_event(event);
2489            }
2490            self.mark_data_edited();
2491        }
2492
2493        undo.push_done_action(journal);
2494        Ok(())
2495    }
2496
2497    fn cellref_to_sheet_row_col(&self, addr: &crate::reference::CellRef) -> (String, u32, u32) {
2498        let sheet = self.graph.sheet_name(addr.sheet_id).to_string();
2499        // Coord stores 0-based indices.
2500        let row = addr.coord.row() + 1;
2501        let col = addr.coord.col() + 1;
2502        (sheet, row, col)
2503    }
2504
2505    fn mirror_undo_batch_to_arrow(
2506        &mut self,
2507        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
2508    ) {
2509        // Undo applies inverses in reverse order.
2510        for item in batch.iter().rev() {
2511            self.mirror_inverse_change_to_arrow(&item.event);
2512        }
2513    }
2514
2515    fn mirror_redo_batch_to_arrow(
2516        &mut self,
2517        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
2518    ) {
2519        // Redo applies events in forward order.
2520        for item in batch.iter() {
2521            self.mirror_forward_change_to_arrow(&item.event);
2522        }
2523    }
2524
2525    fn mirror_inverse_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
2526        use crate::engine::ChangeEvent;
2527        use formualizer_common::LiteralValue;
2528
2529        match ev {
2530            ChangeEvent::SetValue {
2531                addr,
2532                old_value,
2533                old_formula,
2534                ..
2535            } => {
2536                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
2537                if old_formula.is_some() {
2538                    self.clear_delta_overlay_cell(&sheet, row, col);
2539                } else {
2540                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
2541                    self.mirror_value_to_overlay(&sheet, row, col, &v);
2542                }
2543            }
2544            ChangeEvent::SetFormula {
2545                addr,
2546                old_value,
2547                old_formula,
2548                ..
2549            } => {
2550                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
2551                if old_formula.is_some() {
2552                    self.clear_delta_overlay_cell(&sheet, row, col);
2553                } else {
2554                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
2555                    self.mirror_value_to_overlay(&sheet, row, col, &v);
2556                }
2557            }
2558            ChangeEvent::SpillCommitted { old, new, .. } => {
2559                // Inverse: restore `old` (or clear if none).
2560                self.mirror_spill_snapshot(new, /*clear_only=*/ true);
2561                if let Some(snap) = old {
2562                    self.mirror_spill_snapshot(snap, /*clear_only=*/ false);
2563                }
2564            }
2565            ChangeEvent::SpillCleared { old, .. } => {
2566                // Inverse: restore prior spill.
2567                self.mirror_spill_snapshot(old, /*clear_only=*/ false);
2568            }
2569            ChangeEvent::SetRowVisibility { .. } => {
2570                // Engine-side metadata only; no Arrow overlay effect.
2571            }
2572            _ => {}
2573        }
2574    }
2575
2576    fn mirror_forward_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
2577        use crate::engine::ChangeEvent;
2578
2579        match ev {
2580            ChangeEvent::SetValue { addr, new, .. } => {
2581                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
2582                self.mirror_value_to_overlay(&sheet, row, col, new);
2583            }
2584            ChangeEvent::SetFormula { addr, .. } => {
2585                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
2586                self.clear_delta_overlay_cell(&sheet, row, col);
2587                // Keep any computed overlay for this cell as-is; it will be recomputed on demand.
2588            }
2589            ChangeEvent::SpillCommitted { old, new, .. } => {
2590                if let Some(snap) = old {
2591                    self.mirror_spill_snapshot(snap, /*clear_only=*/ true);
2592                }
2593                self.mirror_spill_snapshot(new, /*clear_only=*/ false);
2594            }
2595            ChangeEvent::SpillCleared { old, .. } => {
2596                self.mirror_spill_snapshot(old, /*clear_only=*/ true);
2597            }
2598            ChangeEvent::SetRowVisibility { .. } => {
2599                // Engine-side metadata only; no Arrow overlay effect.
2600            }
2601            _ => {
2602                // Other graph structural operations do not have direct value effects in Arrow.
2603            }
2604        }
2605    }
2606
2607    fn mirror_spill_snapshot(
2608        &mut self,
2609        snap: &crate::engine::graph::editor::change_log::SpillSnapshot,
2610        clear_only: bool,
2611    ) {
2612        use formualizer_common::LiteralValue;
2613
2614        let mut i = 0usize;
2615        for row in &snap.values {
2616            for v in row {
2617                if let Some(cell) = snap.target_cells.get(i) {
2618                    let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
2619                    let out = if clear_only {
2620                        LiteralValue::Empty
2621                    } else {
2622                        v.clone()
2623                    };
2624                    self.mirror_value_to_computed_overlay(&sheet, r, c, &out);
2625                }
2626                i += 1;
2627            }
2628        }
2629        // If target_cells is longer than values (should not happen), clear remaining cells.
2630        if clear_only {
2631            for cell in snap.target_cells.iter().skip(i) {
2632                let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
2633                self.mirror_value_to_computed_overlay(&sheet, r, c, &LiteralValue::Empty);
2634            }
2635        }
2636    }
2637
2638    pub fn set_default_sheet_by_name(&mut self, name: &str) {
2639        self.graph.set_default_sheet_by_name(name);
2640    }
2641
2642    pub fn set_default_sheet_by_id(&mut self, id: SheetId) {
2643        self.graph.set_default_sheet_by_id(id);
2644    }
2645
2646    pub fn set_sheet_index_mode(&mut self, mode: crate::engine::SheetIndexMode) {
2647        self.graph.set_sheet_index_mode(mode);
2648    }
2649
2650    fn clear_cached_static_schedule(&mut self) {
2651        self.cached_static_schedule = None;
2652    }
2653
2654    /// Mark data edited: bump snapshot and set edited flag.
2655    /// Value-only edits keep the stable-topology schedule cache alive.
2656    pub fn mark_data_edited(&mut self) {
2657        self.snapshot_id
2658            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2659        self.has_edited = true;
2660    }
2661
2662    /// Mark a topology-changing edit: bump snapshot + topology epoch and invalidate cached schedules.
2663    pub fn mark_topology_edited(&mut self) {
2664        self.snapshot_id
2665            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2666        self.topology_epoch = self.topology_epoch.wrapping_add(1);
2667        self.clear_cached_static_schedule();
2668        self.has_edited = true;
2669    }
2670
2671    fn mark_all_formula_vertices_dirty(&mut self) {
2672        let vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
2673        for vertex in vertices {
2674            self.graph.mark_vertex_dirty(vertex);
2675        }
2676    }
2677
2678    fn mark_moved_formula_vertices_dirty(
2679        &mut self,
2680        summary: &crate::engine::graph::editor::vertex_editor::ShiftSummary,
2681    ) {
2682        for vertex in &summary.vertices_moved {
2683            if self.graph.get_formula_id(*vertex).is_some() {
2684                self.graph.mark_vertex_dirty(*vertex);
2685            }
2686        }
2687    }
2688
2689    /// Access Arrow sheet store (read-only)
2690    pub fn sheet_store(&self) -> &SheetStore {
2691        &self.arrow_sheets
2692    }
2693
2694    /// Access Arrow sheet store (mutable)
2695    pub fn sheet_store_mut(&mut self) -> &mut SheetStore {
2696        &mut self.arrow_sheets
2697    }
2698
2699    pub fn has_staged_formulas(&self) -> bool {
2700        !self.staged_formulas.is_empty()
2701    }
2702
2703    pub fn staged_formula_count(&self) -> usize {
2704        self.staged_formulas.values().map(Vec::len).sum()
2705    }
2706
2707    pub fn staged_formula_state_snapshot(&self) -> Vec<(String, u32, u32, String)> {
2708        let mut snapshot = Vec::new();
2709        for (sheet, entries) in &self.staged_formulas {
2710            for (row, col, text) in entries {
2711                snapshot.push((sheet.clone(), *row, *col, text.clone()));
2712            }
2713        }
2714        snapshot.sort_by(|a, b| {
2715            a.0.cmp(&b.0)
2716                .then(a.1.cmp(&b.1))
2717                .then(a.2.cmp(&b.2))
2718                .then(a.3.cmp(&b.3))
2719        });
2720        snapshot
2721    }
2722
2723    pub fn restore_staged_formula_state(&mut self, snapshot: &[(String, u32, u32, String)]) {
2724        self.staged_formulas.clear();
2725        for (sheet, row, col, text) in snapshot {
2726            self.stage_formula_text(sheet, *row, *col, text.clone());
2727        }
2728    }
2729
2730    /// Stage a formula text instead of inserting into the graph (used when deferring is enabled).
2731    pub fn stage_formula_text(&mut self, sheet: &str, row: u32, col: u32, text: String) {
2732        let entries = self.staged_formulas.entry(sheet.to_string()).or_default();
2733        if let Some((_, _, existing)) = entries
2734            .iter_mut()
2735            .find(|(existing_row, existing_col, _)| *existing_row == row && *existing_col == col)
2736        {
2737            *existing = text;
2738        } else {
2739            entries.push((row, col, text));
2740        }
2741    }
2742
2743    pub fn clear_staged_formula_text(&mut self, sheet: &str, row: u32, col: u32) -> Option<String> {
2744        let mut removed = None;
2745        let mut remove_sheet = false;
2746        if let Some(entries) = self.staged_formulas.get_mut(sheet) {
2747            if let Some(idx) = entries.iter().position(|(existing_row, existing_col, _)| {
2748                *existing_row == row && *existing_col == col
2749            }) {
2750                let (_, _, text) = entries.remove(idx);
2751                removed = Some(text);
2752            }
2753            remove_sheet = entries.is_empty();
2754        }
2755        if remove_sheet {
2756            self.staged_formulas.remove(sheet);
2757        }
2758        removed
2759    }
2760
2761    pub fn clear_staged_formulas_for_sheet(&mut self, sheet: &str) {
2762        self.staged_formulas.remove(sheet);
2763    }
2764
2765    pub fn rename_staged_formula_sheet(&mut self, old: &str, new: &str) {
2766        let Some(entries) = self.staged_formulas.remove(old) else {
2767            return;
2768        };
2769        for (row, col, text) in entries {
2770            self.stage_formula_text(new, row, col, text);
2771        }
2772    }
2773
2774    /// Get a staged formula text for a given cell if present (cloned).
2775    pub fn get_staged_formula_text(&self, sheet: &str, row: u32, col: u32) -> Option<String> {
2776        self.staged_formulas.get(sheet).and_then(|v| {
2777            v.iter()
2778                .rev()
2779                .find(|(r, c, _)| *r == row && *c == col)
2780                .map(|(_, _, s)| s.clone())
2781        })
2782    }
2783
2784    pub fn formula_parse_diagnostics(&self) -> &[FormulaParseDiagnostic] {
2785        &self.formula_parse_diagnostics
2786    }
2787
2788    pub fn take_formula_parse_diagnostics(&mut self) -> Vec<FormulaParseDiagnostic> {
2789        std::mem::take(&mut self.formula_parse_diagnostics)
2790    }
2791
2792    pub fn clear_formula_parse_diagnostics(&mut self) {
2793        self.formula_parse_diagnostics.clear();
2794    }
2795
2796    pub fn last_formula_ingest_report(&self) -> Option<&FormulaIngestReport> {
2797        self.last_formula_ingest_report.as_ref()
2798    }
2799
2800    pub fn formula_ingest_report_total(&self) -> &FormulaIngestReport {
2801        &self.formula_ingest_report_total
2802    }
2803
2804    #[cfg(test)]
2805    pub(crate) fn last_formula_plane_span_eval_report(&self) -> Option<&SpanEvalReport> {
2806        self.last_formula_plane_span_eval_report.as_ref()
2807    }
2808
2809    #[cfg(test)]
2810    pub(crate) fn formula_plane_indexes_epoch(&self) -> u64 {
2811        self.graph.formula_authority().indexes_epoch()
2812    }
2813
2814    fn record_formula_ingest_report(&mut self, report: FormulaIngestReport) {
2815        self.formula_ingest_report_total.mode = report.mode;
2816        self.formula_ingest_report_total.accumulate(&report);
2817        self.last_formula_ingest_report = Some(report);
2818    }
2819
2820    fn analyze_formula_plane_shadow_candidates(
2821        &mut self,
2822        batches: &[FormulaIngestBatch],
2823    ) -> FormulaIngestReport {
2824        let mut report = FormulaIngestReport::with_mode(FormulaPlaneMode::Shadow);
2825        report.formula_cells_seen = batches.iter().map(|batch| batch.len() as u64).sum();
2826
2827        // Touch graph-owned authority deliberately: Tranche 3 shadow analysis uses
2828        // scratch state, but FormulaPlane ownership now lives on DependencyGraph.
2829        let _active_epoch = self.graph.formula_authority().plane.epoch();
2830
2831        let batch_sheet_ids: Vec<SheetId> = batches
2832            .iter()
2833            .map(|batch| self.graph.sheet_id_mut(&batch.sheet_name))
2834            .collect();
2835        let mut groups: BTreeMap<
2836            (SheetId, u64, u32),
2837            Vec<(FormulaPlacementCandidate, CandidateAnalysis)>,
2838        > = BTreeMap::new();
2839        {
2840            let mut pipeline = self.ingest_pipeline();
2841            for (batch, sheet_id) in batches.iter().zip(batch_sheet_ids.iter().copied()) {
2842                for record in &batch.formulas {
2843                    if record.row == 0 || record.col == 0 {
2844                        report.shadow_candidate_cells =
2845                            report.shadow_candidate_cells.saturating_add(1);
2846                        report.shadow_fallback_cells =
2847                            report.shadow_fallback_cells.saturating_add(1);
2848                        Self::record_shadow_fallback_reason(
2849                            &mut report,
2850                            PlacementFallbackReason::UnsupportedShapeOrGaps,
2851                            1,
2852                        );
2853                        continue;
2854                    }
2855
2856                    let placement = CellRef::new(
2857                        sheet_id,
2858                        Coord::from_excel(record.row, record.col, true, true),
2859                    );
2860                    let ingested = match pipeline.ingest_formula(
2861                        FormulaAstInput::RawArena(record.ast_id),
2862                        placement,
2863                        record.formula_text.clone(),
2864                    ) {
2865                        Ok(ingested) => ingested,
2866                        Err(_) => {
2867                            report.shadow_candidate_cells =
2868                                report.shadow_candidate_cells.saturating_add(1);
2869                            report.shadow_fallback_cells =
2870                                report.shadow_fallback_cells.saturating_add(1);
2871                            Self::record_shadow_fallback_reason(
2872                                &mut report,
2873                                PlacementFallbackReason::UnsupportedCanonicalTemplate,
2874                                1,
2875                            );
2876                            continue;
2877                        }
2878                    };
2879                    let candidate = FormulaPlacementCandidate::new(
2880                        sheet_id,
2881                        record.row - 1,
2882                        record.col - 1,
2883                        ingested.ast_id,
2884                        record.formula_text.clone(),
2885                    );
2886                    let analysis = match CandidateAnalysis::from_ingested(&candidate, &ingested) {
2887                        Ok(analysis) => analysis,
2888                        Err(reason) => {
2889                            report.shadow_candidate_cells =
2890                                report.shadow_candidate_cells.saturating_add(1);
2891                            report.shadow_fallback_cells =
2892                                report.shadow_fallback_cells.saturating_add(1);
2893                            Self::record_shadow_fallback_reason(&mut report, reason, 1);
2894                            continue;
2895                        }
2896                    };
2897                    groups
2898                        .entry((
2899                            sheet_id,
2900                            ingested.parameterized_canonical_hash,
2901                            candidate.col,
2902                        ))
2903                        .or_default()
2904                        .push((candidate, analysis));
2905                }
2906            }
2907        }
2908
2909        let mut scratch_plane = FormulaPlane::default();
2910        for entries in groups.into_values() {
2911            let (candidates, analyses): (Vec<_>, Vec<_>) = entries.into_iter().unzip();
2912            for (component, component_analyses) in
2913                Self::split_candidate_components_with_analyses(candidates, analyses)
2914            {
2915                let placement_report = place_candidate_family_with_analyses(
2916                    &mut scratch_plane,
2917                    component,
2918                    component_analyses,
2919                );
2920                let counters = placement_report.counters;
2921                report.shadow_candidate_cells = report
2922                    .shadow_candidate_cells
2923                    .saturating_add(counters.formula_cells_seen);
2924                report.shadow_accepted_span_cells = report
2925                    .shadow_accepted_span_cells
2926                    .saturating_add(counters.accepted_span_cells);
2927                report.shadow_fallback_cells = report
2928                    .shadow_fallback_cells
2929                    .saturating_add(counters.legacy_cells);
2930                report.shadow_templates_interned = report
2931                    .shadow_templates_interned
2932                    .saturating_add(counters.templates_interned);
2933                report.shadow_spans_created = report
2934                    .shadow_spans_created
2935                    .saturating_add(counters.spans_created);
2936                report.graph_formula_vertices_avoided_shadow = report
2937                    .graph_formula_vertices_avoided_shadow
2938                    .saturating_add(counters.formula_vertices_avoided);
2939                report.ast_roots_avoided_shadow = report
2940                    .ast_roots_avoided_shadow
2941                    .saturating_add(counters.ast_roots_avoided);
2942                report.edge_rows_avoided_shadow = report
2943                    .edge_rows_avoided_shadow
2944                    .saturating_add(counters.edge_rows_avoided);
2945                for (reason, count) in counters.fallback_reasons {
2946                    Self::record_shadow_fallback_reason(&mut report, reason, count);
2947                }
2948            }
2949        }
2950        report
2951    }
2952
2953    fn record_shadow_fallback_reason(
2954        report: &mut FormulaIngestReport,
2955        reason: PlacementFallbackReason,
2956        count: u64,
2957    ) {
2958        *report
2959            .fallback_reasons
2960            .entry(format!("{reason:?}"))
2961            .or_default() += count;
2962    }
2963
2964    fn analyze_formula_plane_authoritative_ingest(
2965        &mut self,
2966        batches: &[FormulaIngestBatch],
2967    ) -> (
2968        FormulaIngestReport,
2969        Vec<FormulaIngestBatch>,
2970        PlannedFormulaMaterialize,
2971    ) {
2972        let mut report =
2973            FormulaIngestReport::with_mode(FormulaPlaneMode::AuthoritativeExperimental);
2974        report.formula_cells_seen = batches.iter().map(|batch| batch.len() as u64).sum();
2975
2976        let mut pending_candidates: Vec<(String, FormulaPlacementCandidate)> = Vec::new();
2977        let mut fallback: BTreeMap<String, Vec<FormulaIngestRecord>> = BTreeMap::new();
2978        let mut planned_fallback: PlannedFormulaMaterialize = BTreeMap::new();
2979
2980        for batch in batches {
2981            let sheet_id = self.graph.sheet_id_mut(&batch.sheet_name);
2982            for record in &batch.formulas {
2983                if record.row == 0 || record.col == 0 {
2984                    report.shadow_candidate_cells = report.shadow_candidate_cells.saturating_add(1);
2985                    report.shadow_fallback_cells = report.shadow_fallback_cells.saturating_add(1);
2986                    Self::record_shadow_fallback_reason(
2987                        &mut report,
2988                        PlacementFallbackReason::UnsupportedShapeOrGaps,
2989                        1,
2990                    );
2991                    fallback
2992                        .entry(batch.sheet_name.clone())
2993                        .or_default()
2994                        .push(record.clone());
2995                    continue;
2996                }
2997
2998                pending_candidates.push((
2999                    batch.sheet_name.clone(),
3000                    FormulaPlacementCandidate::new(
3001                        sheet_id,
3002                        record.row - 1,
3003                        record.col - 1,
3004                        record.ast_id,
3005                        record.formula_text.clone(),
3006                    ),
3007                ));
3008            }
3009        }
3010
3011        let mut groups: BTreeMap<(SheetId, u64, u32), Vec<usize>> = BTreeMap::new();
3012        let mut analyses_by_index: Vec<Option<CandidateAnalysis>> =
3013            (0..pending_candidates.len()).map(|_| None).collect();
3014        let mut plans_by_index: Vec<Option<DependencyPlanRow>> =
3015            (0..pending_candidates.len()).map(|_| None).collect();
3016        {
3017            let mut pipeline = self.ingest_pipeline();
3018            for (idx, (sheet_name, candidate)) in pending_candidates.iter_mut().enumerate() {
3019                let placement = CellRef::new(
3020                    candidate.sheet_id,
3021                    Coord::from_excel(
3022                        candidate.row.saturating_add(1),
3023                        candidate.col.saturating_add(1),
3024                        true,
3025                        true,
3026                    ),
3027                );
3028                let ingested = pipeline.ingest_formula(
3029                    FormulaAstInput::RawArena(candidate.ast_id),
3030                    placement,
3031                    candidate.formula_text.clone(),
3032                );
3033                match ingested {
3034                    Ok(ingested) => {
3035                        candidate.ast_id = ingested.ast_id;
3036                        let canonical_hash = ingested.parameterized_canonical_hash;
3037                        let dep_plan = ingested.dep_plan.clone();
3038                        match CandidateAnalysis::from_ingested(candidate, &ingested) {
3039                            Ok(analysis) => {
3040                                groups
3041                                    .entry((candidate.sheet_id, canonical_hash, candidate.col))
3042                                    .or_default()
3043                                    .push(idx);
3044                                analyses_by_index[idx] = Some(analysis);
3045                                plans_by_index[idx] = Some(dep_plan);
3046                            }
3047                            Err(reason) => {
3048                                report.shadow_candidate_cells =
3049                                    report.shadow_candidate_cells.saturating_add(1);
3050                                report.shadow_fallback_cells =
3051                                    report.shadow_fallback_cells.saturating_add(1);
3052                                Self::record_shadow_fallback_reason(&mut report, reason, 1);
3053                                planned_fallback
3054                                    .entry(sheet_name.clone())
3055                                    .or_default()
3056                                    .push((
3057                                        candidate.row.saturating_add(1),
3058                                        candidate.col.saturating_add(1),
3059                                        candidate.ast_id,
3060                                        dep_plan,
3061                                    ));
3062                            }
3063                        }
3064                    }
3065                    Err(_) => {
3066                        report.shadow_candidate_cells =
3067                            report.shadow_candidate_cells.saturating_add(1);
3068                        report.shadow_fallback_cells =
3069                            report.shadow_fallback_cells.saturating_add(1);
3070                        Self::record_shadow_fallback_reason(
3071                            &mut report,
3072                            PlacementFallbackReason::UnsupportedCanonicalTemplate,
3073                            1,
3074                        );
3075                        fallback.entry(sheet_name.clone()).or_default().push(
3076                            FormulaIngestRecord::new(
3077                                candidate.row.saturating_add(1),
3078                                candidate.col.saturating_add(1),
3079                                candidate.ast_id,
3080                                candidate.formula_text.clone(),
3081                            ),
3082                        );
3083                    }
3084                }
3085            }
3086        }
3087
3088        for ((_sheet_id, _canonical_hash, _col), candidate_indices) in groups {
3089            let sheet_name = pending_candidates[candidate_indices[0]].0.clone();
3090            let mut plans_by_coord: BTreeMap<(u32, u32), Vec<DependencyPlanRow>> = BTreeMap::new();
3091            for idx in &candidate_indices {
3092                if let Some(plan) = plans_by_index[*idx].clone() {
3093                    let candidate = &pending_candidates[*idx].1;
3094                    plans_by_coord
3095                        .entry((candidate.row, candidate.col))
3096                        .or_default()
3097                        .push(plan);
3098                }
3099            }
3100            let candidates: Vec<_> = candidate_indices
3101                .iter()
3102                .map(|idx| pending_candidates[*idx].1.clone())
3103                .collect();
3104            let components = Self::split_shadow_candidate_components(candidates);
3105            let analyzed_components =
3106                if components.len() == 1 && components[0].len() == candidate_indices.len() {
3107                    let component = components.into_iter().next().expect("one component");
3108                    let component_analyses = candidate_indices
3109                        .iter()
3110                        .map(|idx| {
3111                            analyses_by_index[*idx]
3112                                .take()
3113                                .expect("candidate analysis must be used once")
3114                        })
3115                        .collect();
3116                    vec![(component, component_analyses)]
3117                } else {
3118                    let mut indices_by_coord: BTreeMap<(u32, u32), Vec<usize>> = BTreeMap::new();
3119                    for idx in candidate_indices.iter().rev() {
3120                        let candidate = &pending_candidates[*idx].1;
3121                        indices_by_coord
3122                            .entry((candidate.row, candidate.col))
3123                            .or_default()
3124                            .push(*idx);
3125                    }
3126
3127                    components
3128                        .into_iter()
3129                        .map(|component| {
3130                            let mut component_analyses = Vec::with_capacity(component.len());
3131                            for candidate in &component {
3132                                let idx = indices_by_coord
3133                                    .get_mut(&(candidate.row, candidate.col))
3134                                    .and_then(Vec::pop)
3135                                    .expect("component candidate must have a precomputed analysis");
3136                                component_analyses.push(
3137                                    analyses_by_index[idx]
3138                                        .take()
3139                                        .expect("candidate analysis must be used once"),
3140                                );
3141                            }
3142                            (component, component_analyses)
3143                        })
3144                        .collect()
3145                };
3146
3147            for (component, component_analyses) in analyzed_components {
3148                for (component, component_analyses) in
3149                    split_candidate_affine_literal_runs(component, component_analyses)
3150                {
3151                    let placement_report = {
3152                        let authority = self.graph.formula_authority_mut();
3153                        place_candidate_family_with_analyses(
3154                            &mut authority.plane,
3155                            component.clone(),
3156                            component_analyses,
3157                        )
3158                    };
3159                    Self::accumulate_formula_plane_placement_report(&mut report, &placement_report);
3160
3161                    for result in &placement_report.results {
3162                        let FormulaPlacementResult::Legacy { placement, .. } = result else {
3163                            continue;
3164                        };
3165                        if let Some(candidate) = component
3166                            .iter()
3167                            .find(|candidate| candidate.placement() == *placement)
3168                        {
3169                            let plan = plans_by_coord
3170                                .get_mut(&(candidate.row, candidate.col))
3171                                .and_then(Vec::pop);
3172                            if let Some(plan) = plan {
3173                                planned_fallback
3174                                    .entry(sheet_name.clone())
3175                                    .or_default()
3176                                    .push((
3177                                        candidate.row.saturating_add(1),
3178                                        candidate.col.saturating_add(1),
3179                                        candidate.ast_id,
3180                                        plan,
3181                                    ));
3182                            } else {
3183                                fallback.entry(sheet_name.clone()).or_default().push(
3184                                    FormulaIngestRecord::new(
3185                                        candidate.row.saturating_add(1),
3186                                        candidate.col.saturating_add(1),
3187                                        candidate.ast_id,
3188                                        candidate.formula_text.clone(),
3189                                    ),
3190                                );
3191                            }
3192                        }
3193                    }
3194                }
3195            }
3196        }
3197
3198        let _index_report = self.graph.formula_authority_mut().rebuild_indexes();
3199
3200        let fallback_batches = fallback
3201            .into_iter()
3202            .map(|(sheet_name, formulas)| FormulaIngestBatch::new(sheet_name, formulas))
3203            .collect();
3204        (report, fallback_batches, planned_fallback)
3205    }
3206
3207    fn accumulate_formula_plane_placement_report(
3208        report: &mut FormulaIngestReport,
3209        placement_report: &crate::formula_plane::placement::FormulaPlacementReport,
3210    ) {
3211        let counters = &placement_report.counters;
3212        report.shadow_candidate_cells = report
3213            .shadow_candidate_cells
3214            .saturating_add(counters.formula_cells_seen);
3215        report.shadow_accepted_span_cells = report
3216            .shadow_accepted_span_cells
3217            .saturating_add(counters.accepted_span_cells);
3218        report.shadow_fallback_cells = report
3219            .shadow_fallback_cells
3220            .saturating_add(counters.legacy_cells);
3221        report.shadow_templates_interned = report
3222            .shadow_templates_interned
3223            .saturating_add(counters.templates_interned);
3224        report.shadow_spans_created = report
3225            .shadow_spans_created
3226            .saturating_add(counters.spans_created);
3227        report.graph_formula_vertices_avoided_shadow = report
3228            .graph_formula_vertices_avoided_shadow
3229            .saturating_add(counters.formula_vertices_avoided);
3230        report.ast_roots_avoided_shadow = report
3231            .ast_roots_avoided_shadow
3232            .saturating_add(counters.ast_roots_avoided);
3233        report.edge_rows_avoided_shadow = report
3234            .edge_rows_avoided_shadow
3235            .saturating_add(counters.edge_rows_avoided);
3236        for (reason, count) in &counters.fallback_reasons {
3237            Self::record_shadow_fallback_reason(report, *reason, *count);
3238        }
3239    }
3240
3241    fn split_candidate_components_with_analyses(
3242        candidates: Vec<FormulaPlacementCandidate>,
3243        mut analyses: Vec<CandidateAnalysis>,
3244    ) -> Vec<(Vec<FormulaPlacementCandidate>, Vec<CandidateAnalysis>)> {
3245        let components = Self::split_shadow_candidate_components(candidates.clone());
3246        let mut analysis_by_coord: BTreeMap<(u32, u32), Vec<CandidateAnalysis>> = BTreeMap::new();
3247        for (candidate, analysis) in candidates.into_iter().zip(analyses.drain(..)) {
3248            analysis_by_coord
3249                .entry((candidate.row, candidate.col))
3250                .or_default()
3251                .push(analysis);
3252        }
3253        components
3254            .into_iter()
3255            .flat_map(|component| {
3256                let mut component_analyses = Vec::with_capacity(component.len());
3257                for candidate in &component {
3258                    let analysis = analysis_by_coord
3259                        .get_mut(&(candidate.row, candidate.col))
3260                        .and_then(Vec::pop)
3261                        .expect("component candidate must have a precomputed analysis");
3262                    component_analyses.push(analysis);
3263                }
3264                split_candidate_affine_literal_runs(component, component_analyses)
3265            })
3266            .collect()
3267    }
3268
3269    fn split_shadow_candidate_components(
3270        candidates: Vec<FormulaPlacementCandidate>,
3271    ) -> Vec<Vec<FormulaPlacementCandidate>> {
3272        if candidates.len() <= 1 {
3273            return vec![candidates];
3274        }
3275
3276        let mut coord_to_indices: BTreeMap<(u32, u32), Vec<usize>> = BTreeMap::new();
3277        for (idx, candidate) in candidates.iter().enumerate() {
3278            coord_to_indices
3279                .entry((candidate.row, candidate.col))
3280                .or_default()
3281                .push(idx);
3282        }
3283
3284        let mut remaining: BTreeSet<usize> = (0..candidates.len()).collect();
3285        let mut components = Vec::new();
3286        while let Some(&start) = remaining.iter().next() {
3287            remaining.remove(&start);
3288            let mut queue = VecDeque::from([start]);
3289            let mut component_indices = Vec::new();
3290
3291            while let Some(idx) = queue.pop_front() {
3292                component_indices.push(idx);
3293                let candidate = &candidates[idx];
3294                let mut neighbor_coords = Vec::with_capacity(5);
3295                neighbor_coords.push((candidate.row, candidate.col));
3296                if let Some(row) = candidate.row.checked_sub(1) {
3297                    neighbor_coords.push((row, candidate.col));
3298                }
3299                neighbor_coords.push((candidate.row.saturating_add(1), candidate.col));
3300                if let Some(col) = candidate.col.checked_sub(1) {
3301                    neighbor_coords.push((candidate.row, col));
3302                }
3303                neighbor_coords.push((candidate.row, candidate.col.saturating_add(1)));
3304
3305                for coord in neighbor_coords {
3306                    if let Some(indices) = coord_to_indices.get(&coord) {
3307                        for &neighbor in indices {
3308                            if remaining.remove(&neighbor) {
3309                                queue.push_back(neighbor);
3310                            }
3311                        }
3312                    }
3313                }
3314            }
3315
3316            component_indices.sort_by_key(|idx| {
3317                let candidate = &candidates[*idx];
3318                (candidate.row, candidate.col, *idx)
3319            });
3320            components.push(
3321                component_indices
3322                    .into_iter()
3323                    .map(|idx| candidates[idx].clone())
3324                    .collect(),
3325            );
3326        }
3327
3328        components
3329    }
3330
3331    pub fn ingest_formula_batches(
3332        &mut self,
3333        batches: Vec<FormulaIngestBatch>,
3334    ) -> Result<FormulaIngestReport, ExcelError> {
3335        let formula_cells_seen = batches.iter().map(|batch| batch.len() as u64).sum();
3336        let (mut report, materialize_batches, planned_materialize) =
3337            match self.config.formula_plane_mode {
3338                FormulaPlaneMode::Off => (
3339                    FormulaIngestReport::with_mode(FormulaPlaneMode::Off),
3340                    batches,
3341                    BTreeMap::new(),
3342                ),
3343                FormulaPlaneMode::Shadow => (
3344                    self.analyze_formula_plane_shadow_candidates(&batches),
3345                    batches,
3346                    BTreeMap::new(),
3347                ),
3348                FormulaPlaneMode::AuthoritativeExperimental => {
3349                    self.analyze_formula_plane_authoritative_ingest(&batches)
3350                }
3351            };
3352        report.formula_cells_seen = formula_cells_seen;
3353
3354        if !materialize_batches.iter().all(FormulaIngestBatch::is_empty)
3355            || !planned_materialize.is_empty()
3356        {
3357            let mut builder = self.begin_bulk_ingest();
3358            for batch in materialize_batches {
3359                if batch.is_empty() {
3360                    continue;
3361                }
3362                let sheet_id = builder.add_sheet(&batch.sheet_name);
3363                builder.add_formula_ids(
3364                    sheet_id,
3365                    batch
3366                        .formulas
3367                        .into_iter()
3368                        .map(|record| (record.row, record.col, record.ast_id)),
3369                );
3370            }
3371            for (sheet_name, formulas) in planned_materialize {
3372                if formulas.is_empty() {
3373                    continue;
3374                }
3375                let sheet_id = builder.add_sheet(&sheet_name);
3376                builder.add_formula_plans(sheet_id, formulas);
3377            }
3378            let summary = builder.finish()?;
3379            report.graph_formula_cells_materialized = summary.formulas as u64;
3380            report.graph_vertices_created = summary.vertices as u64;
3381            report.graph_edges_created = summary.edges as u64;
3382        }
3383
3384        self.record_formula_ingest_report(report.clone());
3385        Ok(report)
3386    }
3387
3388    pub fn handle_formula_parse_error(
3389        &mut self,
3390        sheet: &str,
3391        row: u32,
3392        col: u32,
3393        formula: &str,
3394        message: String,
3395    ) -> Result<Option<ASTNode>, ExcelError> {
3396        let policy = self.config.formula_parse_policy;
3397
3398        if policy == FormulaParsePolicy::Strict {
3399            let col_a1 = col_letters_from_1based(col).unwrap_or_else(|_| "?".to_string());
3400            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
3401                "Formula parse error at {sheet}!{col_a1}{row}: {message}"
3402            )));
3403        }
3404
3405        self.formula_parse_diagnostics.push(FormulaParseDiagnostic {
3406            sheet: sheet.to_string(),
3407            row,
3408            col,
3409            formula: formula.to_string(),
3410            message: message.clone(),
3411            policy,
3412        });
3413
3414        match policy {
3415            FormulaParsePolicy::Strict => unreachable!(),
3416            FormulaParsePolicy::KeepCachedValue => Ok(None),
3417            FormulaParsePolicy::AsText => Ok(Some(ASTNode::new(
3418                ASTNodeType::Literal(LiteralValue::Text(formula.to_string())),
3419                None,
3420            ))),
3421            FormulaParsePolicy::CoerceToError => {
3422                let err = ExcelError::new(ExcelErrorKind::Error)
3423                    .with_message(format!("Malformed formula: {message}"));
3424                Ok(Some(ASTNode::new(
3425                    ASTNodeType::Literal(LiteralValue::Error(err)),
3426                    None,
3427                )))
3428            }
3429        }
3430    }
3431
3432    /// Build graph for all staged formulas.
3433    pub fn build_graph_all(&mut self) -> Result<(), formualizer_parse::ExcelError> {
3434        if self.staged_formulas.is_empty() {
3435            return Ok(());
3436        }
3437        // Take staged formulas before borrowing graph via builder.
3438        let staged = std::mem::take(&mut self.staged_formulas);
3439        for sheet in staged.keys() {
3440            let _ = self.add_sheet(sheet);
3441        }
3442
3443        // Parse/recover first, then pass prepared batches through the centralized ingest seam.
3444        let mut prepared: PreparedFormulaBatches = Vec::new();
3445        for (sheet, entries) in staged {
3446            let mut formulas: Vec<FormulaIngestRecord> = Vec::new();
3447            let mut cache: rustc_hash::FxHashMap<String, Option<crate::engine::arena::AstNodeId>> =
3448                rustc_hash::FxHashMap::default();
3449            cache.reserve(4096);
3450
3451            for (row, col, txt) in entries {
3452                let key = if txt.starts_with('=') {
3453                    txt
3454                } else {
3455                    format!("={txt}")
3456                };
3457                let ast_id = if let Some(cached) = cache.get(&key) {
3458                    *cached
3459                } else {
3460                    let parsed = match formualizer_parse::parser::parse(&key) {
3461                        Ok(parsed) => Some(parsed),
3462                        Err(e) => {
3463                            self.handle_formula_parse_error(&sheet, row, col, &key, e.to_string())?
3464                        }
3465                    };
3466                    let ast_id = parsed.as_ref().map(|ast| self.intern_formula_ast(ast));
3467                    cache.insert(key.clone(), ast_id);
3468                    ast_id
3469                };
3470
3471                if let Some(ast_id) = ast_id {
3472                    formulas.push(FormulaIngestRecord::new(
3473                        row,
3474                        col,
3475                        ast_id,
3476                        Some(Arc::<str>::from(key.clone())),
3477                    ));
3478                }
3479            }
3480
3481            if !formulas.is_empty() {
3482                prepared.push(FormulaIngestBatch::new(sheet, formulas));
3483            }
3484        }
3485
3486        if !prepared.is_empty() {
3487            let _ = self.ingest_formula_batches(prepared)?;
3488        }
3489        Ok(())
3490    }
3491
3492    /// Build graph for specific sheets (consuming only those staged entries).
3493    pub fn build_graph_for_sheets<'a, I: IntoIterator<Item = &'a str>>(
3494        &mut self,
3495        sheets: I,
3496    ) -> Result<(), formualizer_parse::ExcelError> {
3497        let mut collected: StagedFormulaBatches = Vec::new();
3498        for s in sheets {
3499            if let Some(entries) = self.staged_formulas.remove(s) {
3500                collected.push((s.to_string(), entries));
3501            }
3502        }
3503
3504        if collected.is_empty() {
3505            return Ok(());
3506        }
3507
3508        for (sheet, _) in &collected {
3509            let _ = self.add_sheet(sheet);
3510        }
3511
3512        // Parse/recover first, then pass prepared batches through the centralized ingest seam.
3513        let mut prepared: PreparedFormulaBatches = Vec::new();
3514        let mut cache: rustc_hash::FxHashMap<String, Option<crate::engine::arena::AstNodeId>> =
3515            rustc_hash::FxHashMap::default();
3516        cache.reserve(4096);
3517
3518        for (sheet, entries) in collected {
3519            let mut formulas: Vec<FormulaIngestRecord> = Vec::new();
3520            for (row, col, txt) in entries {
3521                let key = if txt.starts_with('=') {
3522                    txt
3523                } else {
3524                    format!("={txt}")
3525                };
3526                let ast_id = if let Some(cached) = cache.get(&key) {
3527                    *cached
3528                } else {
3529                    let parsed = match formualizer_parse::parser::parse(&key) {
3530                        Ok(parsed) => Some(parsed),
3531                        Err(e) => {
3532                            self.handle_formula_parse_error(&sheet, row, col, &key, e.to_string())?
3533                        }
3534                    };
3535                    let ast_id = parsed.as_ref().map(|ast| self.intern_formula_ast(ast));
3536                    cache.insert(key.clone(), ast_id);
3537                    ast_id
3538                };
3539
3540                if let Some(ast_id) = ast_id {
3541                    formulas.push(FormulaIngestRecord::new(
3542                        row,
3543                        col,
3544                        ast_id,
3545                        Some(Arc::<str>::from(key.clone())),
3546                    ));
3547                }
3548            }
3549            if !formulas.is_empty() {
3550                prepared.push(FormulaIngestBatch::new(sheet, formulas));
3551            }
3552        }
3553
3554        if !prepared.is_empty() {
3555            let _ = self.ingest_formula_batches(prepared)?;
3556        }
3557        Ok(())
3558    }
3559
3560    /// Begin bulk Arrow ingest for base values (Phase A)
3561    pub fn begin_bulk_ingest_arrow(
3562        &mut self,
3563    ) -> crate::engine::arrow_ingest::ArrowBulkIngestBuilder<'_, R> {
3564        crate::engine::arrow_ingest::ArrowBulkIngestBuilder::new(self)
3565    }
3566
3567    /// Begin bulk updates to Arrow store (Phase C)
3568    pub fn begin_bulk_update_arrow(
3569        &mut self,
3570    ) -> crate::engine::arrow_ingest::ArrowBulkUpdateBuilder<'_, R> {
3571        crate::engine::arrow_ingest::ArrowBulkUpdateBuilder::new(self)
3572    }
3573
3574    fn ensure_known_sheet_id(&self, sheet: &str) -> Result<SheetId, crate::engine::EditorError> {
3575        self.graph.sheet_id(sheet).ok_or(
3576            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
3577                name: sheet.to_string(),
3578                reason: "Unknown sheet".to_string(),
3579            },
3580        )
3581    }
3582
3583    fn normalize_row_1based(row_1based: u32) -> Result<u32, crate::engine::EditorError> {
3584        if row_1based == 0 {
3585            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
3586        }
3587        Ok(row_1based - 1)
3588    }
3589
3590    fn normalize_row_range_1based(
3591        start_row_1based: u32,
3592        end_row_1based: u32,
3593    ) -> Result<(u32, u32), crate::engine::EditorError> {
3594        if start_row_1based == 0 || end_row_1based == 0 {
3595            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
3596        }
3597        if start_row_1based > end_row_1based {
3598            return Err(crate::engine::EditorError::TransactionFailed {
3599                reason: "Row range start is greater than end".to_string(),
3600            });
3601        }
3602        Ok((start_row_1based - 1, end_row_1based - 1))
3603    }
3604
3605    fn invalidate_row_visibility_mask_cache(&self) {
3606        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
3607            cache.clear();
3608        }
3609    }
3610
3611    fn set_row_hidden_by_sheet_id(
3612        &mut self,
3613        sheet_id: SheetId,
3614        row0: u32,
3615        hidden: bool,
3616        source: RowVisibilitySource,
3617    ) -> bool {
3618        let changed = {
3619            let state = self.row_visibility.entry(sheet_id).or_default();
3620            state.set_row_hidden(row0, hidden, source)
3621        };
3622
3623        let remove_entry = self
3624            .row_visibility
3625            .get(&sheet_id)
3626            .map(|state| state.is_empty())
3627            .unwrap_or(false);
3628        if remove_entry {
3629            self.row_visibility.remove(&sheet_id);
3630        }
3631
3632        if changed {
3633            self.invalidate_row_visibility_mask_cache();
3634        }
3635
3636        changed
3637    }
3638
3639    fn set_rows_hidden_by_sheet_id(
3640        &mut self,
3641        sheet_id: SheetId,
3642        start_row0: u32,
3643        end_row0: u32,
3644        hidden: bool,
3645        source: RowVisibilitySource,
3646    ) -> bool {
3647        let changed = {
3648            let state = self.row_visibility.entry(sheet_id).or_default();
3649            state.set_rows_hidden(start_row0, end_row0, hidden, source)
3650        };
3651
3652        let remove_entry = self
3653            .row_visibility
3654            .get(&sheet_id)
3655            .map(|state| state.is_empty())
3656            .unwrap_or(false);
3657        if remove_entry {
3658            self.row_visibility.remove(&sheet_id);
3659        }
3660
3661        if changed {
3662            self.invalidate_row_visibility_mask_cache();
3663        }
3664
3665        changed
3666    }
3667
3668    fn shift_row_visibility_insert(&mut self, sheet_id: SheetId, before0: u32, count: u32) {
3669        if count == 0 {
3670            return;
3671        }
3672        let mut changed = false;
3673        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
3674            changed = state.insert_rows(before0, count);
3675            state.is_empty()
3676        } else {
3677            false
3678        };
3679        if remove_entry {
3680            self.row_visibility.remove(&sheet_id);
3681        }
3682        if changed {
3683            self.invalidate_row_visibility_mask_cache();
3684        }
3685    }
3686
3687    fn shift_row_visibility_delete(&mut self, sheet_id: SheetId, start0: u32, count: u32) {
3688        if count == 0 {
3689            return;
3690        }
3691        let mut changed = false;
3692        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
3693            changed = state.delete_rows(start0, count);
3694            state.is_empty()
3695        } else {
3696            false
3697        };
3698        if remove_entry {
3699            self.row_visibility.remove(&sheet_id);
3700        }
3701        if changed {
3702            self.invalidate_row_visibility_mask_cache();
3703        }
3704    }
3705
3706    fn apply_inverse_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
3707        if let crate::engine::ChangeEvent::SetRowVisibility {
3708            sheet_id,
3709            row0,
3710            source,
3711            old_hidden,
3712            ..
3713        } = event
3714        {
3715            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *old_hidden, *source);
3716        }
3717    }
3718
3719    fn apply_forward_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
3720        if let crate::engine::ChangeEvent::SetRowVisibility {
3721            sheet_id,
3722            row0,
3723            source,
3724            new_hidden,
3725            ..
3726        } = event
3727        {
3728            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *new_hidden, *source);
3729        }
3730    }
3731
3732    fn apply_inverse_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
3733        for event in events.iter().rev() {
3734            self.apply_inverse_row_visibility_event(event);
3735        }
3736    }
3737
3738    fn apply_forward_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
3739        for event in events {
3740            self.apply_forward_row_visibility_event(event);
3741        }
3742    }
3743
3744    fn apply_inverse_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
3745        if let crate::engine::ChangeEvent::StagedFormulaStateChanged { before, .. } = event {
3746            self.restore_staged_formula_state(before);
3747        }
3748    }
3749
3750    fn apply_forward_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
3751        if let crate::engine::ChangeEvent::StagedFormulaStateChanged { after, .. } = event {
3752            self.restore_staged_formula_state(after);
3753        }
3754    }
3755
3756    pub fn set_row_hidden(
3757        &mut self,
3758        sheet: &str,
3759        row_1based: u32,
3760        hidden: bool,
3761        source: RowVisibilitySource,
3762    ) -> Result<(), crate::engine::EditorError> {
3763        let sheet_id = self.ensure_known_sheet_id(sheet)?;
3764        let row0 = Self::normalize_row_1based(row_1based)?;
3765        if self.set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source) {
3766            self.record_formula_plane_structural_change(StructuralScope::Region(
3767                Region::whole_row(sheet_id, row0),
3768            ));
3769            self.mark_data_edited();
3770        }
3771        Ok(())
3772    }
3773
3774    pub fn set_rows_hidden(
3775        &mut self,
3776        sheet: &str,
3777        start_row_1based: u32,
3778        end_row_1based: u32,
3779        hidden: bool,
3780        source: RowVisibilitySource,
3781    ) -> Result<(), crate::engine::EditorError> {
3782        let sheet_id = self.ensure_known_sheet_id(sheet)?;
3783        let (start_row0, end_row0) =
3784            Self::normalize_row_range_1based(start_row_1based, end_row_1based)?;
3785        if self.set_rows_hidden_by_sheet_id(sheet_id, start_row0, end_row0, hidden, source) {
3786            if start_row0 == end_row0 {
3787                self.record_formula_plane_structural_change(StructuralScope::Region(
3788                    Region::whole_row(sheet_id, start_row0),
3789                ));
3790            } else {
3791                self.record_formula_plane_structural_change(StructuralScope::Sheet(sheet_id));
3792            }
3793            self.mark_data_edited();
3794        }
3795        Ok(())
3796    }
3797
3798    pub fn is_row_hidden(
3799        &self,
3800        sheet: &str,
3801        row_1based: u32,
3802        source: Option<RowVisibilitySource>,
3803    ) -> Option<bool> {
3804        let sheet_id = self.graph.sheet_id(sheet)?;
3805        let row0 = row_1based.checked_sub(1)?;
3806        Some(
3807            self.row_visibility
3808                .get(&sheet_id)
3809                .map(|state| state.is_row_hidden(row0, source))
3810                .unwrap_or(false),
3811        )
3812    }
3813
3814    pub fn row_visibility_version(&self, sheet: &str) -> Option<u64> {
3815        let sheet_id = self.graph.sheet_id(sheet)?;
3816        Some(
3817            self.row_visibility
3818                .get(&sheet_id)
3819                .map(|state| state.version())
3820                .unwrap_or(0),
3821        )
3822    }
3823
3824    fn build_row_visibility_mask_for_view(
3825        &self,
3826        view: &RangeView<'_>,
3827        mode: VisibilityMaskMode,
3828    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
3829        let sheet_rows = view.sheet().nrows as usize;
3830        if sheet_rows == 0 || view.start_row() >= sheet_rows {
3831            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
3832        }
3833
3834        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
3835        let start_row0 = view.start_row() as u32;
3836        let end_row0 = view.end_row().min(sheet_rows.saturating_sub(1)) as u32;
3837        let version = self
3838            .row_visibility
3839            .get(&sheet_id)
3840            .map(|state| state.version())
3841            .unwrap_or(0);
3842        let key = VisibilityMaskCacheKey {
3843            sheet_id,
3844            start_row0,
3845            end_row0,
3846            mode,
3847            version,
3848        };
3849
3850        if let Ok(cache) = self.row_visibility_mask_cache.read()
3851            && let Some(mask) = cache.get(&key)
3852        {
3853            #[cfg(test)]
3854            visibility_mask_test_hooks::inc_hit();
3855            return Some(mask.clone());
3856        }
3857
3858        #[cfg(test)]
3859        visibility_mask_test_hooks::inc_miss();
3860
3861        let state = self.row_visibility.get(&sheet_id);
3862        let mut out = Vec::with_capacity((end_row0 - start_row0 + 1) as usize);
3863        for row0 in start_row0..=end_row0 {
3864            let manual_hidden = state
3865                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Manual)))
3866                .unwrap_or(false);
3867            let filter_hidden = state
3868                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Filter)))
3869                .unwrap_or(false);
3870
3871            let include = match mode {
3872                VisibilityMaskMode::IncludeAll => true,
3873                VisibilityMaskMode::ExcludeManualHidden => !manual_hidden,
3874                VisibilityMaskMode::ExcludeFilterHidden => !filter_hidden,
3875                VisibilityMaskMode::ExcludeManualOrFilterHidden => {
3876                    !(manual_hidden || filter_hidden)
3877                }
3878            };
3879            out.push(include);
3880        }
3881
3882        let mask = std::sync::Arc::new(arrow_array::BooleanArray::from(out));
3883        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
3884            const MAX_CACHE_ENTRIES: usize = 4096;
3885            if cache.len() >= MAX_CACHE_ENTRIES {
3886                cache.clear();
3887                #[cfg(test)]
3888                visibility_mask_test_hooks::inc_eviction();
3889            }
3890            cache.insert(key, mask.clone());
3891        }
3892
3893        Some(mask)
3894    }
3895
3896    fn editor_error_to_excel(error: crate::engine::EditorError) -> ExcelError {
3897        match error {
3898            crate::engine::EditorError::Excel(error) => error,
3899            other => ExcelError::new(ExcelErrorKind::Value).with_message(other.to_string()),
3900        }
3901    }
3902
3903    fn demote_span_containing_cell_for_write(
3904        &mut self,
3905        sheet_id: SheetId,
3906        row0: u32,
3907        col0: u32,
3908    ) -> Result<(), crate::engine::EditorError> {
3909        if self.config.formula_plane_mode == FormulaPlaneMode::Off {
3910            return Ok(());
3911        }
3912        let placement = PlacementCoord::new(sheet_id, row0, col0);
3913        let inside_active_span = self
3914            .graph
3915            .formula_authority()
3916            .plane
3917            .spans
3918            .find_at(placement)
3919            .is_some();
3920        if inside_active_span {
3921            self.demote_spans_preserving_computed_overlays(
3922                sheet_id,
3923                Region::point(sheet_id, row0, col0),
3924            )?;
3925        }
3926        Ok(())
3927    }
3928
3929    fn demote_spans_preserving_computed_overlays(
3930        &mut self,
3931        _sheet_id: SheetId,
3932        affected_region: Region,
3933    ) -> Result<(), crate::engine::EditorError> {
3934        // Per-cell write inside a span (or whole-sheet demote via remove_sheet):
3935        // not a structural axis shift. Demote every span whose result or read
3936        // region intersects `affected_region`; leave disjoint spans untouched.
3937        self.demote_spans_for_structural_op_impl(None, affected_region, false)
3938    }
3939
3940    fn structural_row_region(sheet_id: SheetId, start_row0: u32) -> Region {
3941        Region::rows_from(sheet_id, start_row0)
3942    }
3943
3944    fn structural_col_region(sheet_id: SheetId, start_col0: u32) -> Region {
3945        Region::cols_from(sheet_id, start_col0)
3946    }
3947
3948    fn span_result_region_intersects_affected(
3949        span: &crate::formula_plane::runtime::FormulaSpan,
3950        affected_region: &Region,
3951    ) -> bool {
3952        Region::from_domain(span.result_region.domain()).intersects(affected_region)
3953    }
3954
3955    fn span_any_read_region_intersects_affected(
3956        plane: &FormulaPlane,
3957        span: &crate::formula_plane::runtime::FormulaSpan,
3958        affected_region: &Region,
3959    ) -> bool {
3960        span.read_summary_id
3961            .and_then(|read_summary_id| plane.span_read_summaries.get(read_summary_id))
3962            .is_some_and(|summary| {
3963                summary
3964                    .dependencies
3965                    .iter()
3966                    .any(|dependency| dependency.read_region.intersects(affected_region))
3967            })
3968    }
3969
3970    fn insert_formula_plane_dirty_coords_for_span(
3971        &self,
3972        span_ref: FormulaSpanRef,
3973        dirty: ProducerDirtyDomain,
3974        out: &mut FxHashSet<(SheetId, u32, u32)>,
3975    ) -> Result<(), crate::engine::EditorError> {
3976        let authority = self.graph.formula_authority();
3977        let span = authority.plane.spans.get(span_ref).ok_or_else(|| {
3978            ExcelError::new(ExcelErrorKind::NImpl)
3979                .with_message("FormulaPlane dirty transfer referenced a stale span")
3980        })?;
3981        match dirty {
3982            ProducerDirtyDomain::Whole => {
3983                out.extend(
3984                    span.domain
3985                        .iter()
3986                        .map(|coord| (coord.sheet_id, coord.row, coord.col)),
3987                );
3988            }
3989            ProducerDirtyDomain::Cells(cells) => {
3990                out.extend(cells.into_iter().filter_map(|key| {
3991                    let coord = PlacementCoord::new(key.sheet_id, key.row, key.col);
3992                    span.domain
3993                        .contains(coord)
3994                        .then_some((coord.sheet_id, coord.row, coord.col))
3995                }));
3996            }
3997            ProducerDirtyDomain::Regions(regions) => {
3998                out.extend(span.domain.iter().filter_map(|coord| {
3999                    let key = crate::formula_plane::region_index::RegionKey::from(coord);
4000                    regions
4001                        .iter()
4002                        .any(|region| region.contains_key(key))
4003                        .then_some((coord.sheet_id, coord.row, coord.col))
4004                }));
4005            }
4006        }
4007        Ok(())
4008    }
4009
4010    fn compute_current_formula_plane_dirty_result_coords(
4011        &self,
4012    ) -> Result<FxHashSet<(SheetId, u32, u32)>, crate::engine::EditorError> {
4013        use crate::formula_plane::producer::compute_dirty_closure;
4014
4015        let authority = self.graph.formula_authority();
4016        let span_refs = authority.active_span_refs();
4017        let span_refs_by_id = span_refs
4018            .iter()
4019            .copied()
4020            .map(|span_ref| (span_ref.id, span_ref))
4021            .collect::<BTreeMap<_, _>>();
4022        let mut dirty_coords = FxHashSet::default();
4023
4024        if self.formula_plane_indexes_epoch_seen != authority.indexes_epoch() {
4025            for span_ref in span_refs {
4026                self.insert_formula_plane_dirty_coords_for_span(
4027                    span_ref,
4028                    ProducerDirtyDomain::Whole,
4029                    &mut dirty_coords,
4030                )?;
4031            }
4032            return Ok(dirty_coords);
4033        }
4034
4035        let pending_changed_regions = authority.pending_changed_regions();
4036        if pending_changed_regions.is_empty() {
4037            return Ok(dirty_coords);
4038        }
4039
4040        let closure = compute_dirty_closure(
4041            &authority.consumer_reads,
4042            pending_changed_regions.iter().copied(),
4043            |producer| authority.producer_results.producer_result_region(producer),
4044        );
4045        for work in closure.work {
4046            let FormulaProducerId::Span(span_id) = work.producer else {
4047                continue;
4048            };
4049            let Some(span_ref) = span_refs_by_id.get(&span_id).copied() else {
4050                continue;
4051            };
4052            self.insert_formula_plane_dirty_coords_for_span(
4053                span_ref,
4054                work.dirty,
4055                &mut dirty_coords,
4056            )?;
4057        }
4058        for fallback in closure.fallbacks {
4059            let FormulaProducerId::Span(span_id) = fallback.consumer else {
4060                continue;
4061            };
4062            let Some(span_ref) = span_refs_by_id.get(&span_id).copied() else {
4063                continue;
4064            };
4065            self.insert_formula_plane_dirty_coords_for_span(
4066                span_ref,
4067                ProducerDirtyDomain::Whole,
4068                &mut dirty_coords,
4069            )?;
4070        }
4071
4072        Ok(dirty_coords)
4073    }
4074
4075    /// Demote active FormulaPlane spans affected by a structural edit on `sheet_id`.
4076    ///
4077    /// This is the conservative Option-A correctness path for structural edits: rather than
4078    /// attempting to transform FormulaPlane span domains/templates/indexes, materialize each span
4079    /// placement as an ordinary legacy graph formula at its current coordinate, remove the span,
4080    /// and let the existing VertexEditor structural machinery shift/delete those vertices and
4081    /// adjust their ASTs.  Spans whose formula domain is on `sheet_id` are affected directly; spans
4082    /// on other sheets are also affected when one of their retained read regions targets
4083    /// `sheet_id`, because those read-region coordinates become stale after row/column shifts.
4084    fn demote_spans_for_structural_op(
4085        &mut self,
4086        op: StructuralOp,
4087        affected_region: Region,
4088    ) -> Result<(), crate::engine::EditorError> {
4089        if op.count() == 0 {
4090            return Ok(());
4091        }
4092        self.demote_spans_for_structural_op_impl(Some(op), affected_region, true)
4093    }
4094
4095    fn demote_spans_for_structural_op_impl(
4096        &mut self,
4097        op: Option<StructuralOp>,
4098        affected_region: Region,
4099        clear_computed_overlays: bool,
4100    ) -> Result<(), crate::engine::EditorError> {
4101        struct SpanPlan {
4102            span_ref: FormulaSpanRef,
4103            sheet_id: SheetId,
4104            ast: ASTNode,
4105            origin_row: u32,
4106            origin_col: u32,
4107            binding_set_id: Option<crate::formula_plane::runtime::SpanBindingSetId>,
4108            placements: Vec<(u32, u32)>,
4109        }
4110
4111        fn substitute_literal_slots_for_template_placement(
4112            ast: &ASTNode,
4113            binding: &[LiteralValue],
4114        ) -> ASTNode {
4115            fn clone_with_slots(
4116                ast: &ASTNode,
4117                binding: &[LiteralValue],
4118                next: &mut usize,
4119                in_array: bool,
4120            ) -> ASTNode {
4121                let node_type = match &ast.node_type {
4122                    ASTNodeType::Literal(_) if !in_array => {
4123                        let value = binding.get(*next).cloned().unwrap_or(LiteralValue::Empty);
4124                        *next = next.saturating_add(1);
4125                        ASTNodeType::Literal(value)
4126                    }
4127                    ASTNodeType::Literal(value) => ASTNodeType::Literal(value.clone()),
4128                    ASTNodeType::Reference {
4129                        original,
4130                        reference,
4131                    } => ASTNodeType::Reference {
4132                        original: original.clone(),
4133                        reference: reference.clone(),
4134                    },
4135                    ASTNodeType::UnaryOp { op, expr } => ASTNodeType::UnaryOp {
4136                        op: op.clone(),
4137                        expr: Box::new(clone_with_slots(expr, binding, next, in_array)),
4138                    },
4139                    ASTNodeType::BinaryOp { op, left, right } => ASTNodeType::BinaryOp {
4140                        op: op.clone(),
4141                        left: Box::new(clone_with_slots(left, binding, next, in_array)),
4142                        right: Box::new(clone_with_slots(right, binding, next, in_array)),
4143                    },
4144                    ASTNodeType::Function { name, args } => ASTNodeType::Function {
4145                        name: name.clone(),
4146                        args: args
4147                            .iter()
4148                            .map(|arg| clone_with_slots(arg, binding, next, in_array))
4149                            .collect(),
4150                    },
4151                    ASTNodeType::Call { callee, args } => ASTNodeType::Call {
4152                        callee: Box::new(clone_with_slots(callee, binding, next, in_array)),
4153                        args: args
4154                            .iter()
4155                            .map(|arg| clone_with_slots(arg, binding, next, in_array))
4156                            .collect(),
4157                    },
4158                    ASTNodeType::Array(rows) => ASTNodeType::Array(
4159                        rows.iter()
4160                            .map(|row| {
4161                                row.iter()
4162                                    .map(|cell| clone_with_slots(cell, binding, next, true))
4163                                    .collect()
4164                            })
4165                            .collect(),
4166                    ),
4167                };
4168                ASTNode::new(node_type, ast.source_token.clone())
4169            }
4170            let mut next = 0usize;
4171            clone_with_slots(ast, binding, &mut next, false)
4172        }
4173
4174        let span_refs = self.graph.formula_authority().active_span_refs();
4175        if span_refs.is_empty() {
4176            return Ok(());
4177        }
4178        let dirty_span_coords = if clear_computed_overlays {
4179            FxHashSet::default()
4180        } else {
4181            self.compute_current_formula_plane_dirty_result_coords()?
4182        };
4183
4184        struct ShiftPlan {
4185            span_ref: FormulaSpanRef,
4186            template_id: crate::formula_plane::ids::FormulaTemplateId,
4187            new_origin_row: u32,
4188            new_origin_col: u32,
4189            new_domain: crate::formula_plane::runtime::PlacementDomain,
4190            new_read_summary: Option<SpanReadSummary>,
4191            binding_set_id: Option<crate::formula_plane::runtime::SpanBindingSetId>,
4192            force_binding_residual_axes: bool,
4193        }
4194
4195        fn checked_shift_u32(value: u32, delta: i64) -> Option<u32> {
4196            u32::try_from(i64::from(value).checked_add(delta)?).ok()
4197        }
4198
4199        fn shifted_read_summary(
4200            read_summary: &SpanReadSummary,
4201            new_result_region: Region,
4202            op: StructuralOp,
4203            row_delta: i64,
4204            col_delta: i64,
4205        ) -> Option<SpanReadSummary> {
4206            let mut dependencies = Vec::with_capacity(read_summary.dependencies.len());
4207            for dependency in &read_summary.dependencies {
4208                let read_region = match op.classify_region(dependency.read_region) {
4209                    crate::formula_plane::structural_shift::AxisShiftCase::OtherSheet
4210                    | crate::formula_plane::structural_shift::AxisShiftCase::EntirelyBelow => {
4211                        dependency.read_region
4212                    }
4213                    crate::formula_plane::structural_shift::AxisShiftCase::EntirelyAboveShift {
4214                        ..
4215                    } => dependency
4216                        .read_region
4217                        .project_through_axis_shift(row_delta, col_delta)?,
4218                    crate::formula_plane::structural_shift::AxisShiftCase::Straddles
4219                    | crate::formula_plane::structural_shift::AxisShiftCase::DeleteFullyContains => {
4220                        return None;
4221                    }
4222                };
4223                dependencies.push(crate::formula_plane::producer::SpanReadDependency {
4224                    read_region,
4225                    projection: dependency.projection,
4226                });
4227            }
4228            Some(SpanReadSummary {
4229                result_region: new_result_region,
4230                dependencies,
4231            })
4232        }
4233
4234        fn compact_axis_through_delete(
4235            min: u32,
4236            max: u32,
4237            start: u32,
4238            count: u32,
4239        ) -> Option<(u32, u32)> {
4240            let end = start.saturating_add(count);
4241            if max < start || min >= end {
4242                return Some((min.saturating_sub(count), max.saturating_sub(count)));
4243            }
4244            let keeps_left = min < start;
4245            let keeps_right = max >= end;
4246            match (keeps_left, keeps_right) {
4247                (false, false) => None,
4248                (true, false) => Some((min, start.checked_sub(1)?)),
4249                (false, true) => Some((start, max.checked_sub(count)?)),
4250                (true, true) => Some((min, max.checked_sub(count)?)),
4251            }
4252        }
4253
4254        fn compact_domain_through_delete(
4255            domain: &PlacementDomain,
4256            op: StructuralOp,
4257        ) -> Option<PlacementDomain> {
4258            match (domain, op) {
4259                (
4260                    PlacementDomain::RowRun {
4261                        sheet_id,
4262                        row_start,
4263                        row_end,
4264                        col,
4265                    },
4266                    StructuralOp::DeleteRows { start, count, .. },
4267                ) => {
4268                    let (row_start, row_end) =
4269                        compact_axis_through_delete(*row_start, *row_end, start, count)?;
4270                    Some(PlacementDomain::row_run(
4271                        *sheet_id, row_start, row_end, *col,
4272                    ))
4273                }
4274                (
4275                    PlacementDomain::Rect {
4276                        sheet_id,
4277                        row_start,
4278                        row_end,
4279                        col_start,
4280                        col_end,
4281                    },
4282                    StructuralOp::DeleteRows { start, count, .. },
4283                ) => {
4284                    let (row_start, row_end) =
4285                        compact_axis_through_delete(*row_start, *row_end, start, count)?;
4286                    Some(PlacementDomain::rect(
4287                        *sheet_id, row_start, row_end, *col_start, *col_end,
4288                    ))
4289                }
4290                (
4291                    PlacementDomain::ColRun {
4292                        sheet_id,
4293                        row,
4294                        col_start,
4295                        col_end,
4296                    },
4297                    StructuralOp::DeleteColumns { start, count, .. },
4298                ) => {
4299                    let (col_start, col_end) =
4300                        compact_axis_through_delete(*col_start, *col_end, start, count)?;
4301                    Some(PlacementDomain::col_run(
4302                        *sheet_id, *row, col_start, col_end,
4303                    ))
4304                }
4305                (
4306                    PlacementDomain::Rect {
4307                        sheet_id,
4308                        row_start,
4309                        row_end,
4310                        col_start,
4311                        col_end,
4312                    },
4313                    StructuralOp::DeleteColumns { start, count, .. },
4314                ) => {
4315                    let (col_start, col_end) =
4316                        compact_axis_through_delete(*col_start, *col_end, start, count)?;
4317                    Some(PlacementDomain::rect(
4318                        *sheet_id, *row_start, *row_end, col_start, col_end,
4319                    ))
4320                }
4321                _ => None,
4322            }
4323        }
4324
4325        fn compact_axis_range_through_delete(
4326            axis: crate::formula_plane::region_index::AxisRange,
4327            start: u32,
4328            count: u32,
4329        ) -> Option<crate::formula_plane::region_index::AxisRange> {
4330            use crate::formula_plane::region_index::AxisRange;
4331            match axis {
4332                AxisRange::Point(point) => compact_axis_through_delete(point, point, start, count)
4333                    .map(|(point, _)| AxisRange::Point(point)),
4334                AxisRange::Span(min, max) => compact_axis_through_delete(min, max, start, count)
4335                    .map(|(min, max)| AxisRange::Span(min, max)),
4336                AxisRange::All => Some(AxisRange::All),
4337                AxisRange::From(_) | AxisRange::To(_) => None,
4338            }
4339        }
4340
4341        fn compact_region_through_delete(region: Region, op: StructuralOp) -> Option<Region> {
4342            let (rows, cols) = region.axis_ranges();
4343            match op {
4344                StructuralOp::DeleteRows {
4345                    sheet_id,
4346                    start,
4347                    count,
4348                } if region.sheet_id() == sheet_id => Some(Region {
4349                    sheet_id,
4350                    rows: compact_axis_range_through_delete(rows, start, count)?,
4351                    cols,
4352                }),
4353                StructuralOp::DeleteColumns {
4354                    sheet_id,
4355                    start,
4356                    count,
4357                } if region.sheet_id() == sheet_id => Some(Region {
4358                    sheet_id,
4359                    rows,
4360                    cols: compact_axis_range_through_delete(cols, start, count)?,
4361                }),
4362                _ => Some(region),
4363            }
4364        }
4365
4366        fn compact_read_summary_through_delete(
4367            read_summary: &SpanReadSummary,
4368            new_result_region: Region,
4369            op: StructuralOp,
4370        ) -> Option<SpanReadSummary> {
4371            let mut dependencies = Vec::with_capacity(read_summary.dependencies.len());
4372            for dependency in &read_summary.dependencies {
4373                let read_region = match op.classify_region(dependency.read_region) {
4374                    crate::formula_plane::structural_shift::AxisShiftCase::OtherSheet
4375                    | crate::formula_plane::structural_shift::AxisShiftCase::EntirelyBelow => {
4376                        dependency.read_region
4377                    }
4378                    crate::formula_plane::structural_shift::AxisShiftCase::EntirelyAboveShift {
4379                        ..
4380                    } => {
4381                        let (row_delta, col_delta) = op.axis_shift_delta();
4382                        dependency
4383                            .read_region
4384                            .project_through_axis_shift(row_delta, col_delta)?
4385                    }
4386                    crate::formula_plane::structural_shift::AxisShiftCase::Straddles => {
4387                        compact_region_through_delete(dependency.read_region, op)?
4388                    }
4389                    crate::formula_plane::structural_shift::AxisShiftCase::DeleteFullyContains => {
4390                        return None;
4391                    }
4392                };
4393                dependencies.push(crate::formula_plane::producer::SpanReadDependency {
4394                    read_region,
4395                    projection: dependency.projection,
4396                });
4397            }
4398            Some(SpanReadSummary {
4399                result_region: new_result_region,
4400                dependencies,
4401            })
4402        }
4403
4404        fn domain_origin_1_based(domain: &PlacementDomain) -> (u32, u32) {
4405            match domain {
4406                PlacementDomain::RowRun { row_start, col, .. } => (row_start + 1, col + 1),
4407                PlacementDomain::ColRun { row, col_start, .. } => (row + 1, col_start + 1),
4408                PlacementDomain::Rect {
4409                    row_start,
4410                    col_start,
4411                    ..
4412                } => (row_start + 1, col_start + 1),
4413            }
4414        }
4415
4416        let mut shift_plans = Vec::new();
4417        let mut remove_refs = Vec::new();
4418        let mut demote_refs = Vec::new();
4419        for span_ref in span_refs {
4420            let authority = self.graph.formula_authority();
4421            let Some(span) = authority.plane.spans.get(span_ref) else {
4422                continue;
4423            };
4424            let read_summary = span
4425                .read_summary_id
4426                .and_then(|id| authority.plane.span_read_summaries.get(id));
4427            let Some(op) = op else {
4428                // Non-structural demote path (per-cell write into span, or
4429                // remove_sheet's whole-sheet sweep). Only demote spans whose
4430                // result or read region intersects affected_region; leave
4431                // disjoint spans untouched.
4432                let result_region_affected =
4433                    Self::span_result_region_intersects_affected(span, &affected_region);
4434                let read_region_affected = Self::span_any_read_region_intersects_affected(
4435                    &authority.plane,
4436                    span,
4437                    &affected_region,
4438                );
4439                if result_region_affected || read_region_affected {
4440                    demote_refs.push(span_ref);
4441                }
4442                continue;
4443            };
4444            match classify_span_for_op(span, read_summary, op) {
4445                SpanShiftPlan::NoOp => {}
4446                SpanShiftPlan::Remove => {
4447                    remove_refs.push(span_ref);
4448                }
4449                SpanShiftPlan::Demote {
4450                    reason:
4451                        crate::formula_plane::structural_shift::SpanDemoteReason::DeletePartiallyOverlaps,
4452                } => {
4453                    let binding_compaction_safe = span
4454                        .binding_set_id
4455                        .and_then(|id| authority.plane.binding_sets.get(id))
4456                        .is_none_or(|binding_set| binding_set.is_single_literal_binding());
4457                    if binding_compaction_safe
4458                        && let Some(new_domain) = compact_domain_through_delete(&span.domain, op)
4459                    {
4460                        let new_result_region = Region::from_domain(&new_domain);
4461                        let new_read_summary = if let Some(summary) = read_summary {
4462                            compact_read_summary_through_delete(summary, new_result_region, op)
4463                        } else {
4464                            None
4465                        };
4466                        if read_summary.is_none() || new_read_summary.is_some() {
4467                            let (new_origin_row, new_origin_col) = domain_origin_1_based(&new_domain);
4468                            let Some(template) = authority.plane.templates.get(span.template_id)
4469                            else {
4470                                return Err(ExcelError::new(ExcelErrorKind::Ref)
4471                                    .with_message(
4472                                        "FormulaPlane delete compaction found a span with a missing template",
4473                                    )
4474                                    .into());
4475                            };
4476                            let force_binding_residual_axes = span
4477                                .binding_set_id
4478                                .and_then(|id| authority.plane.binding_sets.get(id))
4479                                .is_some_and(|binding_set| {
4480                                    !binding_set.value_ref_slots.is_empty()
4481                                        && (new_origin_row != template.origin_row
4482                                            || new_origin_col != template.origin_col)
4483                                });
4484                            shift_plans.push(ShiftPlan {
4485                                span_ref,
4486                                template_id: span.template_id,
4487                                new_origin_row,
4488                                new_origin_col,
4489                                new_domain,
4490                                new_read_summary,
4491                                binding_set_id: span.binding_set_id,
4492                                force_binding_residual_axes,
4493                            });
4494                        } else {
4495                            demote_refs.push(span_ref);
4496                        }
4497                    } else {
4498                        demote_refs.push(span_ref);
4499                    }
4500                }
4501                SpanShiftPlan::Demote { .. } => {
4502                    demote_refs.push(span_ref);
4503                }
4504                SpanShiftPlan::Shift {
4505                    row_delta,
4506                    col_delta,
4507                    origin_row_delta,
4508                    origin_col_delta,
4509                } => {
4510                    let Some(template) = authority.plane.templates.get(span.template_id) else {
4511                        return Err(ExcelError::new(ExcelErrorKind::Ref)
4512                            .with_message("FormulaPlane shift found a span with a missing template")
4513                            .into());
4514                    };
4515                    let Some(new_origin_row) =
4516                        checked_shift_u32(template.origin_row, origin_row_delta)
4517                    else {
4518                        return Err(ExcelError::new(ExcelErrorKind::Ref)
4519                            .with_message("FormulaPlane shift overflowed template origin row")
4520                            .into());
4521                    };
4522                    let Some(new_origin_col) =
4523                        checked_shift_u32(template.origin_col, origin_col_delta)
4524                    else {
4525                        return Err(ExcelError::new(ExcelErrorKind::Ref)
4526                            .with_message("FormulaPlane shift overflowed template origin column")
4527                            .into());
4528                    };
4529                    let Some(new_domain) =
4530                        span.domain.project_through_axis_shift(row_delta, col_delta)
4531                    else {
4532                        return Err(ExcelError::new(ExcelErrorKind::Ref)
4533                            .with_message("FormulaPlane shift overflowed span domain")
4534                            .into());
4535                    };
4536                    let new_result_region = Region::from_domain(&new_domain);
4537                    let new_read_summary = if let Some(summary) = read_summary {
4538                        Some(
4539                            shifted_read_summary(
4540                                summary,
4541                                new_result_region,
4542                                op,
4543                                row_delta,
4544                                col_delta,
4545                            )
4546                            .ok_or_else(|| {
4547                                ExcelError::new(ExcelErrorKind::Ref).with_message(
4548                                    "FormulaPlane shift could not project read summary",
4549                                )
4550                            })?,
4551                        )
4552                    } else {
4553                        None
4554                    };
4555                    let force_binding_residual_axes = span
4556                        .binding_set_id
4557                        .and_then(|id| authority.plane.binding_sets.get(id))
4558                        .is_some_and(|binding_set| {
4559                            !binding_set.value_ref_slots.is_empty()
4560                                && (origin_row_delta != 0 || origin_col_delta != 0)
4561                        });
4562                    shift_plans.push(ShiftPlan {
4563                        span_ref,
4564                        template_id: span.template_id,
4565                        new_origin_row,
4566                        new_origin_col,
4567                        new_domain,
4568                        new_read_summary,
4569                        binding_set_id: span.binding_set_id,
4570                        force_binding_residual_axes,
4571                    });
4572                }
4573            }
4574        }
4575        if !shift_plans.is_empty() || !remove_refs.is_empty() {
4576            let authority = self.graph.formula_authority_mut();
4577            for span_ref in remove_refs {
4578                authority.plane.remove_overlays_for_source_span(span_ref);
4579                authority.plane.remove_span(span_ref);
4580            }
4581            for plan in shift_plans {
4582                let Some(template_id) = authority.plane.intern_shifted_template_origin(
4583                    plan.template_id,
4584                    plan.new_origin_row,
4585                    plan.new_origin_col,
4586                ) else {
4587                    return Err(ExcelError::new(ExcelErrorKind::Ref)
4588                        .with_message("FormulaPlane shift could not clone template origin")
4589                        .into());
4590                };
4591                if let Some(binding_set_id) = plan.binding_set_id {
4592                    let Some(template) = authority.plane.templates.get(template_id) else {
4593                        return Err(ExcelError::new(ExcelErrorKind::Ref)
4594                            .with_message("FormulaPlane shift could not find shifted template")
4595                            .into());
4596                    };
4597                    let (ast_id, origin_row, origin_col) =
4598                        (template.ast_id, template.origin_row, template.origin_col);
4599                    authority.plane.set_binding_template_anchor(
4600                        binding_set_id,
4601                        ast_id,
4602                        origin_row,
4603                        origin_col,
4604                    );
4605                }
4606                let read_summary_id = plan
4607                    .new_read_summary
4608                    .map(|summary| authority.plane.insert_span_read_summary(summary));
4609                let result_region = ResultRegion::scalar_cells(plan.new_domain.clone());
4610                if !authority.plane.replace_span_geometry(
4611                    plan.span_ref,
4612                    template_id,
4613                    plan.new_domain,
4614                    result_region,
4615                    read_summary_id,
4616                ) {
4617                    return Err(ExcelError::new(ExcelErrorKind::Ref)
4618                        .with_message("FormulaPlane shift could not update span geometry")
4619                        .into());
4620                }
4621                if plan.force_binding_residual_axes
4622                    && let Some(binding_set_id) = plan.binding_set_id
4623                {
4624                    // Value-ref memoization keys are placement-relative. When a
4625                    // structural op moves the formula origin while keeping some
4626                    // precedents fixed (e.g. insert a column before a formula
4627                    // family that reads column A), those keys no longer name
4628                    // the same producer cells. Keep correctness by forcing
4629                    // placement offsets into the key so memoization falls back
4630                    // to per-placement work rather than broadcasting stale
4631                    // representative values.
4632                    authority.plane.force_binding_residual_axes(binding_set_id);
4633                }
4634            }
4635            authority.rebuild_indexes();
4636            self.formula_plane_indexes_epoch_seen = 0;
4637        }
4638
4639        let mut span_plans = Vec::new();
4640        for span_ref in demote_refs {
4641            let authority = self.graph.formula_authority();
4642            let Some(span) = authority.plane.spans.get(span_ref) else {
4643                continue;
4644            };
4645            let Some(template) = authority.plane.templates.get(span.template_id) else {
4646                return Err(ExcelError::new(ExcelErrorKind::Ref)
4647                    .with_message("FormulaPlane demotion found a span with a missing template")
4648                    .into());
4649            };
4650            let ast = self
4651                .graph
4652                .data_store()
4653                .retrieve_ast(template.ast_id, self.graph.sheet_reg())
4654                .ok_or_else(|| {
4655                    ExcelError::new(ExcelErrorKind::Ref)
4656                        .with_message("FormulaPlane demotion could not retrieve the template AST")
4657                })?;
4658            let placements = span
4659                .domain
4660                .iter()
4661                .map(|placement| (placement.row + 1, placement.col + 1))
4662                .collect();
4663            span_plans.push(SpanPlan {
4664                span_ref,
4665                sheet_id: span.sheet_id,
4666                ast,
4667                origin_row: template.origin_row,
4668                origin_col: template.origin_col,
4669                binding_set_id: span.binding_set_id,
4670                placements,
4671            });
4672        }
4673        if span_plans.is_empty() {
4674            return Ok(());
4675        }
4676
4677        let mut relocated = Vec::new();
4678        let mut placement_cells = Vec::new();
4679        for plan in &span_plans {
4680            for &(row, col) in &plan.placements {
4681                let row_delta = i64::from(row) - i64::from(plan.origin_row);
4682                let col_delta = i64::from(col) - i64::from(plan.origin_col);
4683                let bound_ast = if let Some(binding_set_id) = plan.binding_set_id {
4684                    let authority = self.graph.formula_authority();
4685                    if let Some(binding_set) = authority.plane.binding_sets.get(binding_set_id) {
4686                        if binding_set.is_single_literal_binding() {
4687                            plan.ast.clone()
4688                        } else {
4689                            let placement = crate::formula_plane::runtime::PlacementCoord::new(
4690                                plan.sheet_id,
4691                                row.saturating_sub(1),
4692                                col.saturating_sub(1),
4693                            );
4694                            let binding =
4695                                authority.plane.spans.get(plan.span_ref).and_then(|span| {
4696                                    binding_set
4697                                        .literal_bindings_for_placement(&span.domain, placement)
4698                                });
4699                            if let Some(binding) = binding {
4700                                substitute_literal_slots_for_template_placement(
4701                                    &plan.ast,
4702                                    binding.as_ref(),
4703                                )
4704                            } else {
4705                                plan.ast.clone()
4706                            }
4707                        }
4708                    } else {
4709                        plan.ast.clone()
4710                    }
4711                } else {
4712                    plan.ast.clone()
4713                };
4714                let ast = relocate_ast_for_template_placement(&bound_ast, row_delta, col_delta)?;
4715                relocated.push((plan.sheet_id, row, col, ast));
4716                placement_cells.push((plan.sheet_id, row, col));
4717            }
4718        }
4719        let planned_by_sheet = {
4720            let mut pipeline = self.ingest_pipeline();
4721            let mut planned_by_sheet: BTreeMap<
4722                SheetId,
4723                Vec<(u32, u32, AstNodeId, DependencyPlanRow)>,
4724            > = BTreeMap::new();
4725            for (formula_sheet_id, row, col, ast) in relocated {
4726                let placement =
4727                    CellRef::new(formula_sheet_id, Coord::from_excel(row, col, true, true));
4728                let ingested =
4729                    pipeline.ingest_formula(FormulaAstInput::Tree(ast), placement, None)?;
4730                planned_by_sheet.entry(formula_sheet_id).or_default().push((
4731                    row,
4732                    col,
4733                    ingested.ast_id,
4734                    ingested.dep_plan,
4735                ));
4736            }
4737            planned_by_sheet
4738        };
4739        {
4740            let authority = self.graph.formula_authority_mut();
4741            for plan in &span_plans {
4742                authority
4743                    .plane
4744                    .remove_overlays_for_source_span(plan.span_ref);
4745                authority.plane.remove_span(plan.span_ref);
4746            }
4747            authority.rebuild_indexes();
4748        }
4749        if clear_computed_overlays {
4750            // Only clear placement cells whose coordinate intersects the affected
4751            // structural region. The structural-op contract preserves cells
4752            // BEFORE the structural boundary; the legacy `clear_computed_overlay_after_*`
4753            // call honors that. Demoting a span whose footprint straddles the
4754            // boundary still must not clear cells before the boundary, even
4755            // though the span as a whole is demoted.
4756            self.clear_computed_overlay_cells_in_region(&placement_cells, &affected_region);
4757        }
4758        for (formula_sheet_id, planned) in planned_by_sheet {
4759            let sheet_name = self.graph.sheet_name(formula_sheet_id).to_string();
4760            self.graph
4761                .bulk_set_formulas_with_plans(&sheet_name, planned)?;
4762        }
4763        if !clear_computed_overlays {
4764            for (formula_sheet_id, row, col) in &placement_cells {
4765                let row0 = row.saturating_sub(1);
4766                let col0 = col.saturating_sub(1);
4767                if dirty_span_coords.contains(&(*formula_sheet_id, row0, col0)) {
4768                    continue;
4769                }
4770                let cell =
4771                    CellRef::new(*formula_sheet_id, Coord::from_excel(*row, *col, true, true));
4772                if let Some(&vertex_id) = self.graph.get_vertex_id_for_address(&cell) {
4773                    self.graph.set_dirty(vertex_id, false);
4774                }
4775            }
4776        }
4777        self.formula_plane_indexes_epoch_seen = 0;
4778        Ok(())
4779    }
4780
4781    /// Insert rows (1-based) and mirror into Arrow store when enabled
4782    pub fn insert_rows(
4783        &mut self,
4784        sheet: &str,
4785        before: u32,
4786        count: u32,
4787    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
4788    {
4789        use crate::engine::graph::editor::vertex_editor::VertexEditor;
4790        let sheet_id = self.ensure_known_sheet_id(sheet)?;
4791        let before0 = before.saturating_sub(1);
4792        let affected_region = Self::structural_row_region(sheet_id, before0);
4793        let op = StructuralOp::InsertRows {
4794            sheet_id,
4795            before: before0,
4796            count,
4797        };
4798        self.demote_spans_for_structural_op(op, affected_region)?;
4799        let summary = {
4800            let mut editor = VertexEditor::new(&mut self.graph);
4801            editor.insert_rows(sheet_id, before0, count)?
4802        };
4803        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
4804            let before0 = before0 as usize;
4805            asheet.insert_rows(before0, count as usize);
4806        }
4807        self.mark_moved_formula_vertices_dirty(&summary);
4808        self.clear_computed_overlay_after_row(sheet, before0 as usize);
4809        self.shift_row_visibility_insert(sheet_id, before0, count);
4810        self.record_formula_plane_structural_change(StructuralScope::Region(affected_region));
4811        self.mark_topology_edited();
4812        Ok(summary)
4813    }
4814
4815    /// Delete rows (1-based) and mirror into Arrow store when enabled
4816    pub fn delete_rows(
4817        &mut self,
4818        sheet: &str,
4819        start: u32,
4820        count: u32,
4821    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
4822    {
4823        use crate::engine::graph::editor::vertex_editor::VertexEditor;
4824        let sheet_id = self.ensure_known_sheet_id(sheet)?;
4825        let start0 = start.saturating_sub(1);
4826        let affected_region = Self::structural_row_region(sheet_id, start0);
4827        let op = StructuralOp::DeleteRows {
4828            sheet_id,
4829            start: start0,
4830            count,
4831        };
4832        self.demote_spans_for_structural_op(op, affected_region)?;
4833        let summary = {
4834            let mut editor = VertexEditor::new(&mut self.graph);
4835            editor.delete_rows(sheet_id, start0, count)?
4836        };
4837        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
4838            let start0 = start0 as usize;
4839            asheet.delete_rows(start0, count as usize);
4840        }
4841        self.mark_moved_formula_vertices_dirty(&summary);
4842        self.clear_computed_overlay_after_row(sheet, start0 as usize);
4843        self.shift_row_visibility_delete(sheet_id, start0, count);
4844        self.record_formula_plane_structural_change(StructuralScope::Region(affected_region));
4845        self.mark_topology_edited();
4846        Ok(summary)
4847    }
4848
4849    /// Insert columns (1-based) and mirror into Arrow store when enabled
4850    pub fn insert_columns(
4851        &mut self,
4852        sheet: &str,
4853        before: u32,
4854        count: u32,
4855    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
4856    {
4857        use crate::engine::graph::editor::vertex_editor::VertexEditor;
4858        let sheet_id = self.graph.sheet_id(sheet).ok_or(
4859            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
4860                name: sheet.to_string(),
4861                reason: "Unknown sheet".to_string(),
4862            },
4863        )?;
4864        let before0 = before.saturating_sub(1);
4865        let affected_region = Self::structural_col_region(sheet_id, before0);
4866        let op = StructuralOp::InsertColumns {
4867            sheet_id,
4868            before: before0,
4869            count,
4870        };
4871        self.demote_spans_for_structural_op(op, affected_region)?;
4872        let summary = {
4873            let mut editor = VertexEditor::new(&mut self.graph);
4874            editor.insert_columns(sheet_id, before0, count)?
4875        };
4876        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
4877            let before0 = before0 as usize;
4878            asheet.insert_columns(before0, count as usize);
4879        }
4880        self.mark_moved_formula_vertices_dirty(&summary);
4881        self.clear_computed_overlay_after_col(sheet, before0 as usize);
4882        self.record_formula_plane_structural_change(StructuralScope::Region(affected_region));
4883        self.mark_topology_edited();
4884        Ok(summary)
4885    }
4886
4887    /// Delete columns (1-based) and mirror into Arrow store when enabled
4888    pub fn delete_columns(
4889        &mut self,
4890        sheet: &str,
4891        start: u32,
4892        count: u32,
4893    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
4894    {
4895        use crate::engine::graph::editor::vertex_editor::VertexEditor;
4896        let sheet_id = self.graph.sheet_id(sheet).ok_or(
4897            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
4898                name: sheet.to_string(),
4899                reason: "Unknown sheet".to_string(),
4900            },
4901        )?;
4902        let start0 = start.saturating_sub(1);
4903        let affected_region = Self::structural_col_region(sheet_id, start0);
4904        let op = StructuralOp::DeleteColumns {
4905            sheet_id,
4906            start: start0,
4907            count,
4908        };
4909        self.demote_spans_for_structural_op(op, affected_region)?;
4910        let summary = {
4911            let mut editor = VertexEditor::new(&mut self.graph);
4912            editor.delete_columns(sheet_id, start0, count)?
4913        };
4914        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
4915            let start0 = start0 as usize;
4916            asheet.delete_columns(start0, count as usize);
4917        }
4918        self.mark_moved_formula_vertices_dirty(&summary);
4919        self.clear_computed_overlay_after_col(sheet, start0 as usize);
4920        self.record_formula_plane_structural_change(StructuralScope::Region(affected_region));
4921        self.mark_topology_edited();
4922        Ok(summary)
4923    }
4924    /// Arrow-backed used row bounds across a column span (1-based inclusive cols).
4925    fn arrow_used_row_bounds(
4926        &self,
4927        sheet: &str,
4928        start_col: u32,
4929        end_col: u32,
4930    ) -> Option<(u32, u32)> {
4931        let a = self.sheet_store().sheet(sheet)?;
4932        if a.columns.is_empty() {
4933            return None;
4934        }
4935        let sc0 = start_col.saturating_sub(1) as usize;
4936        let ec0 = end_col.saturating_sub(1) as usize;
4937        let col_hi = a.columns.len().saturating_sub(1);
4938        if sc0 > col_hi {
4939            return None;
4940        }
4941        let ec0 = ec0.min(col_hi);
4942        // Pass-scoped cache with snapshot guard
4943        let snap = self.data_snapshot_id();
4944        let mut min_r0: Option<usize> = None;
4945        for ci in sc0..=ec0 {
4946            let sheet_id = self.graph.sheet_id(sheet)?;
4947            if let Some((Some(mv), _)) = self.row_bounds_cache.read().ok().and_then(|g| {
4948                g.as_ref()
4949                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
4950            }) {
4951                let mv = mv as usize;
4952                min_r0 = Some(min_r0.map(|m| m.min(mv)).unwrap_or(mv));
4953                continue;
4954            }
4955            // Compute and store
4956            let (min_c, max_c) = Self::scan_column_used_bounds(a, ci);
4957            if let Ok(mut g) = self.row_bounds_cache.write() {
4958                g.get_or_insert_with(|| RowBoundsCache::new(snap))
4959                    .put_row_bounds(sheet_id, ci, snap, (min_c, max_c));
4960            }
4961            if let Some(m) = min_c {
4962                min_r0 = Some(min_r0.map(|mm| mm.min(m as usize)).unwrap_or(m as usize));
4963            }
4964        }
4965        min_r0?;
4966        let mut max_r0: Option<usize> = None;
4967        for ci in sc0..=ec0 {
4968            let sheet_id = self.graph.sheet_id(sheet)?;
4969            if let Some((_, Some(mv))) = self.row_bounds_cache.read().ok().and_then(|g| {
4970                g.as_ref()
4971                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
4972            }) {
4973                let mv = mv as usize;
4974                max_r0 = Some(max_r0.map(|m| m.max(mv)).unwrap_or(mv));
4975                continue;
4976            }
4977            let (_min_c, max_c) = Self::scan_column_used_bounds(a, ci);
4978            if let Ok(mut g) = self.row_bounds_cache.write() {
4979                g.get_or_insert_with(|| RowBoundsCache::new(snap))
4980                    .put_row_bounds(sheet_id, ci, snap, (_min_c, max_c));
4981            }
4982            if let Some(m) = max_c {
4983                max_r0 = Some(max_r0.map(|mm| mm.max(m as usize)).unwrap_or(m as usize));
4984            }
4985        }
4986        match (min_r0, max_r0) {
4987            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
4988            _ => None,
4989        }
4990    }
4991
4992    fn scan_column_used_bounds(
4993        a: &crate::arrow_store::ArrowSheet,
4994        ci: usize,
4995    ) -> (Option<u32>, Option<u32>) {
4996        let col = &a.columns[ci];
4997
4998        // Min: scan dense chunks first, then sparse chunks in ascending index order.
4999        let mut min_r0: Option<u32> = None;
5000        for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
5001            let tags = chunk.type_tag.values();
5002            for (off, &t) in tags.iter().enumerate() {
5003                let overlay_non_empty = chunk
5004                    .overlay
5005                    .get(off)
5006                    .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
5007                    .unwrap_or(false)
5008                    || chunk
5009                        .computed_overlay
5010                        .get(off)
5011                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
5012                        .unwrap_or(false);
5013                if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
5014                    let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
5015                        break;
5016                    };
5017                    let row0 = chunk_start + off;
5018                    min_r0 = Some(row0 as u32);
5019                    break;
5020                }
5021            }
5022            if min_r0.is_some() {
5023                break;
5024            }
5025        }
5026        if min_r0.is_none() && !col.sparse_chunks.is_empty() {
5027            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
5028            sparse_idxs.sort_unstable();
5029            for chunk_idx in sparse_idxs {
5030                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
5031                    continue;
5032                };
5033                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
5034                    continue;
5035                };
5036                let tags = chunk.type_tag.values();
5037                for (off, &t) in tags.iter().enumerate() {
5038                    let overlay_non_empty = chunk
5039                        .overlay
5040                        .get(off)
5041                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
5042                        .unwrap_or(false)
5043                        || chunk
5044                            .computed_overlay
5045                            .get(off)
5046                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
5047                            .unwrap_or(false);
5048                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
5049                        let row0 = chunk_start + off;
5050                        min_r0 = Some(row0 as u32);
5051                        break;
5052                    }
5053                }
5054                if min_r0.is_some() {
5055                    break;
5056                }
5057            }
5058        }
5059
5060        // Max: scan sparse chunks in descending index order, then dense chunks in reverse.
5061        let mut max_r0: Option<u32> = None;
5062        if !col.sparse_chunks.is_empty() {
5063            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
5064            sparse_idxs.sort_unstable_by(|a, b| b.cmp(a));
5065            for chunk_idx in sparse_idxs {
5066                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
5067                    continue;
5068                };
5069                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
5070                    continue;
5071                };
5072                let tags = chunk.type_tag.values();
5073                for (rev_idx, &t) in tags.iter().enumerate().rev() {
5074                    let overlay_non_empty = chunk
5075                        .overlay
5076                        .get(rev_idx)
5077                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
5078                        .unwrap_or(false)
5079                        || chunk
5080                            .computed_overlay
5081                            .get(rev_idx)
5082                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
5083                            .unwrap_or(false);
5084                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
5085                        let row0 = chunk_start + rev_idx;
5086                        max_r0 = Some(row0 as u32);
5087                        break;
5088                    }
5089                }
5090                if max_r0.is_some() {
5091                    break;
5092                }
5093            }
5094        }
5095        if max_r0.is_none() {
5096            for (chunk_idx, chunk) in col.chunks.iter().enumerate().rev() {
5097                let tags = chunk.type_tag.values();
5098                for (rev_idx, &t) in tags.iter().enumerate().rev() {
5099                    let overlay_non_empty = chunk
5100                        .overlay
5101                        .get(rev_idx)
5102                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
5103                        .unwrap_or(false)
5104                        || chunk
5105                            .computed_overlay
5106                            .get(rev_idx)
5107                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
5108                            .unwrap_or(false);
5109                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
5110                        let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
5111                            break;
5112                        };
5113                        let row0 = chunk_start + rev_idx;
5114                        max_r0 = Some(row0 as u32);
5115                        break;
5116                    }
5117                }
5118                if max_r0.is_some() {
5119                    break;
5120                }
5121            }
5122        }
5123
5124        (min_r0, max_r0)
5125    }
5126
5127    /// Arrow-backed used column bounds across a row span (1-based inclusive rows).
5128    fn arrow_used_col_bounds(
5129        &self,
5130        sheet: &str,
5131        start_row: u32,
5132        end_row: u32,
5133    ) -> Option<(u32, u32)> {
5134        let a = self.sheet_store().sheet(sheet)?;
5135        if a.columns.is_empty() {
5136            return None;
5137        }
5138        let sr0 = start_row.saturating_sub(1) as usize;
5139        let er0 = end_row.saturating_sub(1) as usize;
5140        if sr0 > er0 {
5141            return None;
5142        }
5143        // Map start/end rows into chunk ranges
5144        // We will scan each column for any non-empty within [sr0..=er0]
5145        let mut min_c0: Option<usize> = None;
5146        let mut max_c0: Option<usize> = None;
5147        // Precompute chunk bounds for row range
5148        for (ci, col) in a.columns.iter().enumerate() {
5149            let mut any_in_range = false;
5150
5151            let scan_chunk = |chunk_idx: usize, chunk: &crate::arrow_store::ColumnChunk| -> bool {
5152                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
5153                    return false;
5154                };
5155                let chunk_len = chunk.type_tag.len();
5156                if chunk_len == 0 {
5157                    return false;
5158                }
5159                let chunk_end = chunk_start + chunk_len.saturating_sub(1);
5160                // check intersection
5161                if sr0 > chunk_end || er0 < chunk_start {
5162                    return false;
5163                }
5164                let start_off = sr0.max(chunk_start) - chunk_start;
5165                let end_off = er0.min(chunk_end) - chunk_start;
5166                let tags = chunk.type_tag.values();
5167                for off in start_off..=end_off {
5168                    let overlay_non_empty = chunk
5169                        .overlay
5170                        .get(off)
5171                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
5172                        .unwrap_or(false)
5173                        || chunk
5174                            .computed_overlay
5175                            .get(off)
5176                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
5177                            .unwrap_or(false);
5178                    if overlay_non_empty || tags[off] != crate::arrow_store::TypeTag::Empty as u8 {
5179                        return true;
5180                    }
5181                }
5182                false
5183            };
5184
5185            for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
5186                if scan_chunk(chunk_idx, chunk) {
5187                    any_in_range = true;
5188                    break;
5189                }
5190            }
5191
5192            if !any_in_range && !col.sparse_chunks.is_empty() {
5193                for (&chunk_idx, chunk) in col.sparse_chunks.iter() {
5194                    if scan_chunk(chunk_idx, chunk) {
5195                        any_in_range = true;
5196                        break;
5197                    }
5198                }
5199            }
5200
5201            if any_in_range {
5202                min_c0 = Some(min_c0.map(|m| m.min(ci)).unwrap_or(ci));
5203                max_c0 = Some(max_c0.map(|m| m.max(ci)).unwrap_or(ci));
5204            }
5205        }
5206        match (min_c0, max_c0) {
5207            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
5208            _ => None,
5209        }
5210    }
5211
5212    fn formula_row_bounds_for_columns(
5213        &self,
5214        sheet: &str,
5215        start_col: u32,
5216        end_col: u32,
5217    ) -> Option<(u32, u32)> {
5218        let sheet_id = self.graph.sheet_id(sheet)?;
5219        let sc0 = start_col.saturating_sub(1);
5220        let ec0 = end_col.saturating_sub(1);
5221        let mut min_r0: Option<u32> = None;
5222        let mut max_r0: Option<u32> = None;
5223
5224        if let Some(index) = self.graph.sheet_index(sheet_id) {
5225            for vid in index.vertices_in_col_range(sc0, ec0) {
5226                if !matches!(
5227                    self.graph.get_vertex_kind(vid),
5228                    VertexKind::FormulaScalar | VertexKind::FormulaArray
5229                ) {
5230                    continue;
5231                }
5232                let row0 = self.graph.vertex_coord(vid).row();
5233                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
5234                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
5235            }
5236        } else {
5237            for vid in self.graph.vertices_in_sheet(sheet_id) {
5238                if !matches!(
5239                    self.graph.get_vertex_kind(vid),
5240                    VertexKind::FormulaScalar | VertexKind::FormulaArray
5241                ) {
5242                    continue;
5243                }
5244                let coord = self.graph.vertex_coord(vid);
5245                let col0 = coord.col();
5246                if col0 < sc0 || col0 > ec0 {
5247                    continue;
5248                }
5249                let row0 = coord.row();
5250                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
5251                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
5252            }
5253        }
5254
5255        match (min_r0, max_r0) {
5256            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
5257            _ => None,
5258        }
5259    }
5260
5261    fn formula_col_bounds_for_rows(
5262        &self,
5263        sheet: &str,
5264        start_row: u32,
5265        end_row: u32,
5266    ) -> Option<(u32, u32)> {
5267        let sheet_id = self.graph.sheet_id(sheet)?;
5268        let sr0 = start_row.saturating_sub(1);
5269        let er0 = end_row.saturating_sub(1);
5270        let mut min_c0: Option<u32> = None;
5271        let mut max_c0: Option<u32> = None;
5272
5273        if let Some(index) = self.graph.sheet_index(sheet_id) {
5274            for vid in index.vertices_in_row_range(sr0, er0) {
5275                if !matches!(
5276                    self.graph.get_vertex_kind(vid),
5277                    VertexKind::FormulaScalar | VertexKind::FormulaArray
5278                ) {
5279                    continue;
5280                }
5281                let col0 = self.graph.vertex_coord(vid).col();
5282                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
5283                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
5284            }
5285        } else {
5286            for vid in self.graph.vertices_in_sheet(sheet_id) {
5287                if !matches!(
5288                    self.graph.get_vertex_kind(vid),
5289                    VertexKind::FormulaScalar | VertexKind::FormulaArray
5290                ) {
5291                    continue;
5292                }
5293                let coord = self.graph.vertex_coord(vid);
5294                let row0 = coord.row();
5295                if row0 < sr0 || row0 > er0 {
5296                    continue;
5297                }
5298                let col0 = coord.col();
5299                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
5300                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
5301            }
5302        }
5303
5304        match (min_c0, max_c0) {
5305            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
5306            _ => None,
5307        }
5308    }
5309
5310    fn union_used_bounds(
5311        first: Option<(u32, u32)>,
5312        second: Option<(u32, u32)>,
5313    ) -> Option<(u32, u32)> {
5314        match (first, second) {
5315            (Some((a0, b0)), Some((a1, b1))) => Some((a0.min(a1), b0.max(b1))),
5316            (Some(bounds), None) | (None, Some(bounds)) => Some(bounds),
5317            (None, None) => None,
5318        }
5319    }
5320
5321    /// Mirror a single cell value into the Arrow overlay if enabled.
5322    /// Handles capacity growth, per-chunk overlay set, and heuristic compaction.
5323    fn mirror_value_to_overlay(&mut self, sheet: &str, row: u32, col: u32, value: &LiteralValue) {
5324        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
5325            return;
5326        }
5327        if self.arrow_sheets.sheet(sheet).is_none() {
5328            self.arrow_sheets
5329                .sheets
5330                .push(crate::arrow_store::ArrowSheet {
5331                    name: std::sync::Arc::<str>::from(sheet),
5332                    columns: Vec::new(),
5333                    nrows: 0,
5334                    chunk_starts: Vec::new(),
5335                    chunk_rows: 32 * 1024,
5336                });
5337        }
5338
5339        let row0 = row.saturating_sub(1) as usize;
5340        let col0 = col.saturating_sub(1) as usize;
5341
5342        let asheet = self
5343            .arrow_sheets
5344            .sheet_mut(sheet)
5345            .expect("ArrowSheet must exist");
5346
5347        let cur_cols = asheet.columns.len();
5348        if col0 >= cur_cols {
5349            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
5350        }
5351
5352        if row0 >= asheet.nrows as usize {
5353            if asheet.columns.is_empty() {
5354                asheet.insert_columns(0, 1);
5355            }
5356            asheet.ensure_row_capacity(row0 + 1);
5357        }
5358        if let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) {
5359            use crate::arrow_store::OverlayValue;
5360            let ov = match value {
5361                LiteralValue::Empty => OverlayValue::Empty,
5362                LiteralValue::Int(i) => OverlayValue::Number(*i as f64),
5363                LiteralValue::Number(n) => OverlayValue::Number(*n),
5364                LiteralValue::Boolean(b) => OverlayValue::Boolean(*b),
5365                LiteralValue::Text(s) => OverlayValue::Text(std::sync::Arc::from(s.clone())),
5366                LiteralValue::Error(e) => {
5367                    OverlayValue::Error(crate::arrow_store::map_error_code(e.kind))
5368                }
5369                LiteralValue::Date(d) => {
5370                    let dt = d.and_hms_opt(0, 0, 0).unwrap();
5371                    let serial = crate::builtins::datetime::datetime_to_serial_for(
5372                        self.config.date_system,
5373                        &dt,
5374                    );
5375                    OverlayValue::DateTime(serial)
5376                }
5377                LiteralValue::DateTime(dt) => {
5378                    let serial = crate::builtins::datetime::datetime_to_serial_for(
5379                        self.config.date_system,
5380                        dt,
5381                    );
5382                    OverlayValue::DateTime(serial)
5383                }
5384                LiteralValue::Time(t) => {
5385                    let serial = t.num_seconds_from_midnight() as f64 / 86_400.0;
5386                    OverlayValue::DateTime(serial)
5387                }
5388                LiteralValue::Duration(d) => {
5389                    let serial = d.num_seconds() as f64 / 86_400.0;
5390                    OverlayValue::Duration(serial)
5391                }
5392                LiteralValue::Pending => OverlayValue::Pending,
5393                LiteralValue::Array(_) => OverlayValue::Error(crate::arrow_store::map_error_code(
5394                    formualizer_common::ExcelErrorKind::Value,
5395                )),
5396            };
5397            let computed_delta = if let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) {
5398                let _ = ch.overlay.set(in_off, ov);
5399                // A user edit must invalidate any computed (formula/spill) overlay entry at
5400                // this cell. Otherwise, if the delta overlay later compacts into the base lanes
5401                // (clearing `overlay`), a stale `computed_overlay=Empty` could incorrectly mask
5402                // the edited base value under the read cascade.
5403                ch.computed_overlay.remove(in_off)
5404            } else {
5405                return;
5406            };
5407            // Heuristic compaction: > len/50 or > 1024
5408            let abs_threshold = 1024usize;
5409            let frac_den = 50usize;
5410            let freed = asheet.maybe_compact_chunk(col0, ch_idx, abs_threshold, frac_den);
5411            if freed > 0 {
5412                self.overlay_compactions = self.overlay_compactions.saturating_add(1);
5413            }
5414            self.adjust_computed_overlay_bytes(computed_delta);
5415        }
5416    }
5417
5418    /// Remove a delta-overlay entry for a single cell (if present).
5419    ///
5420    /// This is used when transitioning a cell to a formula so that any previous user-edit overlay
5421    /// does not continue to mask computed overlay outputs.
5422    fn clear_delta_overlay_cell(&mut self, sheet: &str, row: u32, col: u32) {
5423        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
5424            return;
5425        }
5426        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
5427            return;
5428        };
5429        let row0 = row.saturating_sub(1) as usize;
5430        let col0 = col.saturating_sub(1) as usize;
5431        if row0 >= asheet.nrows as usize {
5432            return;
5433        }
5434        if col0 >= asheet.columns.len() {
5435            return;
5436        }
5437        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
5438            return;
5439        };
5440        if let Some(ch) = asheet.columns[col0].chunk_mut(ch_idx) {
5441            let _ = ch.overlay.remove(in_off);
5442        }
5443    }
5444
5445    fn clear_computed_overlay_col_row_range(
5446        &mut self,
5447        sheet: &str,
5448        col0: usize,
5449        start_row0: usize,
5450        end_row0_exclusive: usize,
5451    ) {
5452        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
5453            return;
5454        }
5455        if start_row0 >= end_row0_exclusive {
5456            return;
5457        }
5458
5459        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
5460            return;
5461        };
5462        if col0 >= asheet.columns.len() || start_row0 >= asheet.nrows as usize {
5463            return;
5464        }
5465        let end_row0_exclusive = end_row0_exclusive.min(asheet.nrows as usize);
5466        if start_row0 >= end_row0_exclusive {
5467            return;
5468        }
5469
5470        let starts = asheet.chunk_starts.clone();
5471        let nrows = asheet.nrows as usize;
5472        let mut delta = 0isize;
5473        let Some(col) = asheet.columns.get_mut(col0) else {
5474            return;
5475        };
5476        for (chunk_idx, ch) in col.chunks.iter_mut().enumerate() {
5477            let Some(&chunk_start) = starts.get(chunk_idx) else {
5478                continue;
5479            };
5480            let chunk_end = starts
5481                .get(chunk_idx + 1)
5482                .copied()
5483                .unwrap_or(nrows)
5484                .min(chunk_start.saturating_add(ch.len()));
5485            let clear_start = start_row0.max(chunk_start);
5486            let clear_end = end_row0_exclusive.min(chunk_end);
5487            if clear_start >= clear_end {
5488                continue;
5489            }
5490            if clear_start == chunk_start && clear_end == chunk_end {
5491                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
5492            } else {
5493                let start_in_chunk = clear_start.saturating_sub(chunk_start).min(ch.len());
5494                let end_in_chunk = clear_end.saturating_sub(chunk_start).min(ch.len());
5495                delta = delta.saturating_add(
5496                    ch.computed_overlay
5497                        .remove_range(start_in_chunk..end_in_chunk),
5498                );
5499            }
5500        }
5501        for (chunk_idx, ch) in &mut col.sparse_chunks {
5502            let Some(&chunk_start) = starts.get(*chunk_idx) else {
5503                continue;
5504            };
5505            let chunk_end = starts
5506                .get(*chunk_idx + 1)
5507                .copied()
5508                .unwrap_or(nrows)
5509                .min(chunk_start.saturating_add(ch.len()));
5510            let clear_start = start_row0.max(chunk_start);
5511            let clear_end = end_row0_exclusive.min(chunk_end);
5512            if clear_start >= clear_end {
5513                continue;
5514            }
5515            if clear_start == chunk_start && clear_end == chunk_end {
5516                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
5517            } else {
5518                let start_in_chunk = clear_start.saturating_sub(chunk_start).min(ch.len());
5519                let end_in_chunk = clear_end.saturating_sub(chunk_start).min(ch.len());
5520                delta = delta.saturating_add(
5521                    ch.computed_overlay
5522                        .remove_range(start_in_chunk..end_in_chunk),
5523                );
5524            }
5525        }
5526        self.adjust_computed_overlay_bytes(delta);
5527    }
5528
5529    fn clear_computed_overlay_cells_in_region(
5530        &mut self,
5531        cells: &[(SheetId, u32, u32)],
5532        affected_region: &Region,
5533    ) {
5534        let mut by_col: BTreeMap<(SheetId, u32), Vec<u32>> = BTreeMap::new();
5535        for (formula_sheet_id, row, col) in cells {
5536            let row0 = row.saturating_sub(1);
5537            let col0 = col.saturating_sub(1);
5538            let placement_region = Region::point(*formula_sheet_id, row0, col0);
5539            if placement_region.intersects(affected_region) {
5540                by_col
5541                    .entry((*formula_sheet_id, col0))
5542                    .or_default()
5543                    .push(row0);
5544            }
5545        }
5546
5547        for ((formula_sheet_id, col0), mut rows) in by_col {
5548            rows.sort_unstable();
5549            rows.dedup();
5550            let sheet_name = self.graph.sheet_name(formula_sheet_id).to_string();
5551            let mut start = rows[0];
5552            let mut prev = rows[0];
5553            for row in rows.into_iter().skip(1) {
5554                if row == prev.saturating_add(1) {
5555                    prev = row;
5556                    continue;
5557                }
5558                self.clear_computed_overlay_col_row_range(
5559                    &sheet_name,
5560                    col0 as usize,
5561                    start as usize,
5562                    prev.saturating_add(1) as usize,
5563                );
5564                start = row;
5565                prev = row;
5566            }
5567            self.clear_computed_overlay_col_row_range(
5568                &sheet_name,
5569                col0 as usize,
5570                start as usize,
5571                prev.saturating_add(1) as usize,
5572            );
5573        }
5574    }
5575
5576    fn clear_computed_overlay_after_row(&mut self, sheet: &str, start_row0: usize) {
5577        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
5578            return;
5579        }
5580
5581        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
5582            return;
5583        };
5584        if start_row0 >= asheet.nrows as usize {
5585            return;
5586        }
5587
5588        let starts = asheet.chunk_starts.clone();
5589        let nrows = asheet.nrows as usize;
5590        let mut delta = 0isize;
5591        for col in &mut asheet.columns {
5592            for (chunk_idx, ch) in col.chunks.iter_mut().enumerate() {
5593                let Some(&chunk_start) = starts.get(chunk_idx) else {
5594                    continue;
5595                };
5596                let chunk_end = starts
5597                    .get(chunk_idx + 1)
5598                    .copied()
5599                    .unwrap_or(nrows)
5600                    .min(chunk_start.saturating_add(ch.len()));
5601                if chunk_end <= start_row0 {
5602                    continue;
5603                }
5604                if chunk_start >= start_row0 {
5605                    delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
5606                } else {
5607                    let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
5608                    delta = delta
5609                        .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
5610                }
5611            }
5612
5613            for (chunk_idx, ch) in &mut col.sparse_chunks {
5614                let Some(&chunk_start) = starts.get(*chunk_idx) else {
5615                    continue;
5616                };
5617                let chunk_end = starts
5618                    .get(*chunk_idx + 1)
5619                    .copied()
5620                    .unwrap_or(nrows)
5621                    .min(chunk_start.saturating_add(ch.len()));
5622                if chunk_end <= start_row0 {
5623                    continue;
5624                }
5625                if chunk_start >= start_row0 {
5626                    delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
5627                } else {
5628                    let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
5629                    delta = delta
5630                        .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
5631                }
5632            }
5633        }
5634        self.adjust_computed_overlay_bytes(delta);
5635    }
5636
5637    fn clear_computed_overlay_after_col(&mut self, sheet: &str, start_col0: usize) {
5638        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
5639            return;
5640        }
5641
5642        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
5643            return;
5644        };
5645        if start_col0 >= asheet.columns.len() {
5646            return;
5647        }
5648
5649        let mut delta = 0isize;
5650        for col in asheet.columns.iter_mut().skip(start_col0) {
5651            for ch in &mut col.chunks {
5652                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
5653            }
5654            for ch in col.sparse_chunks.values_mut() {
5655                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
5656            }
5657        }
5658        self.adjust_computed_overlay_bytes(delta);
5659    }
5660
5661    #[inline]
5662    fn literal_to_overlay_value(&self, value: &LiteralValue) -> crate::arrow_store::OverlayValue {
5663        use crate::arrow_store::OverlayValue;
5664        match value {
5665            LiteralValue::Empty => OverlayValue::Empty,
5666            LiteralValue::Int(i) => OverlayValue::Number(*i as f64),
5667            LiteralValue::Number(n) => OverlayValue::Number(*n),
5668            LiteralValue::Boolean(b) => OverlayValue::Boolean(*b),
5669            LiteralValue::Text(s) => OverlayValue::Text(std::sync::Arc::from(s.clone())),
5670            LiteralValue::Error(e) => {
5671                OverlayValue::Error(crate::arrow_store::map_error_code(e.kind))
5672            }
5673            LiteralValue::Date(d) => {
5674                let dt = d.and_hms_opt(0, 0, 0).unwrap();
5675                let serial =
5676                    crate::builtins::datetime::datetime_to_serial_for(self.config.date_system, &dt);
5677                OverlayValue::DateTime(serial)
5678            }
5679            LiteralValue::DateTime(dt) => {
5680                let serial =
5681                    crate::builtins::datetime::datetime_to_serial_for(self.config.date_system, dt);
5682                OverlayValue::DateTime(serial)
5683            }
5684            LiteralValue::Time(t) => {
5685                let serial = t.num_seconds_from_midnight() as f64 / 86_400.0;
5686                OverlayValue::DateTime(serial)
5687            }
5688            LiteralValue::Duration(d) => {
5689                let serial = d.num_seconds() as f64 / 86_400.0;
5690                OverlayValue::Duration(serial)
5691            }
5692            LiteralValue::Pending => OverlayValue::Pending,
5693            LiteralValue::Array(_) => OverlayValue::Error(crate::arrow_store::map_error_code(
5694                formualizer_common::ExcelErrorKind::Value,
5695            )),
5696        }
5697    }
5698
5699    /// Read a single cell's delta overlay entry (if present), preserving the distinction between
5700    /// absent and explicit `Empty`.
5701    fn read_delta_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
5702        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
5703            return None;
5704        }
5705        let asheet = self.arrow_sheets.sheet(sheet)?;
5706        let row0 = row.saturating_sub(1) as usize;
5707        let col0 = col.saturating_sub(1) as usize;
5708        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
5709            return None;
5710        }
5711        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
5712        let ch = asheet.columns[col0].chunk(ch_idx)?;
5713        ch.overlay.get_scalar(in_off).map(|ov| ov.to_literal())
5714    }
5715
5716    /// Read a single cell's computed overlay entry (if present), preserving the distinction
5717    /// between absent and explicit `Empty`.
5718    fn read_computed_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
5719        if !(self.config.arrow_storage_enabled
5720            && self.config.delta_overlay_enabled
5721            && self.config.write_formula_overlay_enabled)
5722        {
5723            return None;
5724        }
5725        let asheet = self.arrow_sheets.sheet(sheet)?;
5726        let row0 = row.saturating_sub(1) as usize;
5727        let col0 = col.saturating_sub(1) as usize;
5728        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
5729            return None;
5730        }
5731        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
5732        let ch = asheet.columns[col0].chunk(ch_idx)?;
5733        ch.computed_overlay
5734            .get_scalar(in_off)
5735            .map(|ov| ov.to_literal())
5736    }
5737
5738    fn set_delta_overlay_cell_raw(
5739        &mut self,
5740        sheet: &str,
5741        row: u32,
5742        col: u32,
5743        value: Option<LiteralValue>,
5744    ) {
5745        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
5746            return;
5747        }
5748
5749        self.ensure_arrow_sheet(sheet);
5750        let ov_opt = value.as_ref().map(|v| self.literal_to_overlay_value(v));
5751        let row0 = row.saturating_sub(1) as usize;
5752        let col0 = col.saturating_sub(1) as usize;
5753        let asheet = self
5754            .arrow_sheets
5755            .sheet_mut(sheet)
5756            .expect("ArrowSheet must exist");
5757
5758        let cur_cols = asheet.columns.len();
5759        if col0 >= cur_cols {
5760            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
5761        }
5762        if row0 >= asheet.nrows as usize {
5763            if asheet.columns.is_empty() {
5764                asheet.insert_columns(0, 1);
5765            }
5766            asheet.ensure_row_capacity(row0 + 1);
5767        }
5768
5769        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
5770            return;
5771        };
5772        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
5773            return;
5774        };
5775
5776        if let Some(ov) = ov_opt {
5777            let _ = ch.overlay.set(in_off, ov);
5778        } else {
5779            let _ = ch.overlay.remove(in_off);
5780        }
5781    }
5782
5783    fn set_computed_overlay_cell_raw(
5784        &mut self,
5785        sheet: &str,
5786        row: u32,
5787        col: u32,
5788        value: Option<LiteralValue>,
5789    ) {
5790        if !(self.config.arrow_storage_enabled
5791            && self.config.delta_overlay_enabled
5792            && self.config.write_formula_overlay_enabled)
5793        {
5794            return;
5795        }
5796
5797        self.ensure_arrow_sheet(sheet);
5798        let ov_opt = value.as_ref().map(|v| self.literal_to_overlay_value(v));
5799        let row0 = row.saturating_sub(1) as usize;
5800        let col0 = col.saturating_sub(1) as usize;
5801        let asheet = self
5802            .arrow_sheets
5803            .sheet_mut(sheet)
5804            .expect("ArrowSheet must exist");
5805
5806        let cur_cols = asheet.columns.len();
5807        if col0 >= cur_cols {
5808            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
5809        }
5810        if row0 >= asheet.nrows as usize {
5811            if asheet.columns.is_empty() {
5812                asheet.insert_columns(0, 1);
5813            }
5814            asheet.ensure_row_capacity(row0 + 1);
5815        }
5816
5817        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
5818            return;
5819        };
5820        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
5821            return;
5822        };
5823
5824        let delta = if let Some(ov) = ov_opt {
5825            ch.computed_overlay.set(in_off, ov)
5826        } else {
5827            ch.computed_overlay.remove(in_off)
5828        };
5829        self.adjust_computed_overlay_bytes(delta);
5830    }
5831
5832    fn apply_arrow_undo_batch(&mut self, batch: &crate::engine::ArrowUndoBatch, undo: bool) {
5833        use crate::engine::ArrowOp;
5834
5835        let iter: Box<dyn Iterator<Item = &ArrowOp>> = if undo {
5836            Box::new(batch.ops.iter().rev())
5837        } else {
5838            Box::new(batch.ops.iter())
5839        };
5840
5841        for op in iter {
5842            match op {
5843                ArrowOp::SetDeltaCell {
5844                    sheet_id,
5845                    row0,
5846                    col0,
5847                    old,
5848                    new,
5849                } => {
5850                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
5851                    let v = if undo { old.clone() } else { new.clone() };
5852                    self.set_delta_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
5853                }
5854                ArrowOp::SetComputedCell {
5855                    sheet_id,
5856                    row0,
5857                    col0,
5858                    old,
5859                    new,
5860                } => {
5861                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
5862                    let v = if undo { old.clone() } else { new.clone() };
5863                    self.set_computed_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
5864                }
5865                ArrowOp::RestoreComputedRect {
5866                    sheet_id,
5867                    sr0,
5868                    sc0,
5869                    er0,
5870                    ec0,
5871                    old,
5872                    new,
5873                } => {
5874                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
5875                    let vals = if undo { old } else { new };
5876                    let height = (*er0).saturating_sub(*sr0) as usize + 1;
5877                    let width = (*ec0).saturating_sub(*sc0) as usize + 1;
5878                    for r in 0..height {
5879                        for c in 0..width {
5880                            let v = vals
5881                                .get(r)
5882                                .and_then(|row| row.get(c))
5883                                .cloned()
5884                                .unwrap_or(LiteralValue::Empty);
5885                            self.set_computed_overlay_cell_raw(
5886                                &sheet,
5887                                *sr0 + 1 + r as u32,
5888                                *sc0 + 1 + c as u32,
5889                                Some(v),
5890                            );
5891                        }
5892                    }
5893                }
5894                ArrowOp::InsertRows {
5895                    sheet_id,
5896                    before0,
5897                    count,
5898                } => {
5899                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
5900                    self.ensure_arrow_sheet(&sheet);
5901                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
5902                        if undo {
5903                            asheet.delete_rows(*before0 as usize, *count as usize);
5904                        } else {
5905                            asheet.insert_rows(*before0 as usize, *count as usize);
5906                        }
5907                    }
5908                }
5909                ArrowOp::InsertCols {
5910                    sheet_id,
5911                    before0,
5912                    count,
5913                } => {
5914                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
5915                    self.ensure_arrow_sheet(&sheet);
5916                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
5917                        if undo {
5918                            asheet.delete_columns(*before0 as usize, *count as usize);
5919                        } else {
5920                            asheet.insert_columns(*before0 as usize, *count as usize);
5921                        }
5922                    }
5923                }
5924            }
5925        }
5926    }
5927
5928    fn record_spill_ops_into_arrow_undo(
5929        &mut self,
5930        undo: &mut crate::engine::ArrowUndoBatch,
5931        events: &[crate::engine::ChangeEvent],
5932    ) {
5933        use crate::engine::ChangeEvent;
5934        use formualizer_common::LiteralValue;
5935
5936        #[allow(clippy::type_complexity)]
5937        let rect_from_snapshot =
5938            |snap: &crate::engine::graph::editor::change_log::SpillSnapshot|
5939             -> Option<(SheetId, u32, u32, u32, u32, Vec<Vec<LiteralValue>>)> {
5940                if snap.target_cells.is_empty() {
5941                    return None;
5942                }
5943                let sheet_id = snap.target_cells[0].sheet_id;
5944                let sr0 = snap.target_cells[0].coord.row();
5945                let sc0 = snap.target_cells[0].coord.col();
5946                if snap.values.is_empty() || snap.values[0].is_empty() {
5947                    return None;
5948                }
5949                let h = snap.values.len() as u32;
5950                let w = snap.values[0].len() as u32;
5951                let er0 = sr0.saturating_add(h.saturating_sub(1));
5952                let ec0 = sc0.saturating_add(w.saturating_sub(1));
5953                Some((sheet_id, sr0, sc0, er0, ec0, snap.values.clone()))
5954            };
5955
5956        for ev in events {
5957            match ev {
5958                ChangeEvent::SpillCommitted { old, new, .. } => {
5959                    if let Some((sid, sr0, sc0, er0, ec0, new_vals)) = rect_from_snapshot(new) {
5960                        let old_vals = if let Some(old_snap) = old {
5961                            rect_from_snapshot(old_snap)
5962                                .map(|(_, _, _, _, _, v)| v)
5963                                .unwrap_or_else(|| {
5964                                    vec![
5965                                        vec![LiteralValue::Empty; new_vals[0].len()];
5966                                        new_vals.len()
5967                                    ]
5968                                })
5969                        } else {
5970                            vec![vec![LiteralValue::Empty; new_vals[0].len()]; new_vals.len()]
5971                        };
5972                        undo.record_restore_computed_rect(
5973                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
5974                        );
5975                    }
5976                }
5977                ChangeEvent::SpillCleared { old, .. } => {
5978                    if let Some((sid, sr0, sc0, er0, ec0, old_vals)) = rect_from_snapshot(old) {
5979                        let new_vals =
5980                            vec![vec![LiteralValue::Empty; old_vals[0].len()]; old_vals.len()];
5981                        undo.record_restore_computed_rect(
5982                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
5983                        );
5984                    }
5985                }
5986                _ => {}
5987            }
5988        }
5989    }
5990
5991    /// Mirror a value into the computed overlay (formula/spill outputs).
5992    ///
5993    /// This path is subject to `EvalConfig.max_overlay_memory_bytes`.
5994    /// If the cap is exceeded, computed overlays are compacted into base lanes.
5995    fn mirror_value_to_computed_overlay(
5996        &mut self,
5997        sheet: &str,
5998        row: u32,
5999        col: u32,
6000        value: &LiteralValue,
6001    ) {
6002        if !(self.config.arrow_storage_enabled
6003            && self.config.delta_overlay_enabled
6004            && self.config.write_formula_overlay_enabled)
6005        {
6006            return;
6007        }
6008        if self.computed_overlay_mirroring_disabled {
6009            return;
6010        }
6011
6012        let ov = self.literal_to_overlay_value(value);
6013        self.write_computed_overlay_value_0based(
6014            sheet,
6015            row.saturating_sub(1),
6016            col.saturating_sub(1),
6017            ov,
6018        );
6019    }
6020
6021    fn write_computed_overlay_value_0based(
6022        &mut self,
6023        sheet: &str,
6024        row0: u32,
6025        col0: u32,
6026        value: OverlayValue,
6027    ) {
6028        if !(self.config.arrow_storage_enabled
6029            && self.config.delta_overlay_enabled
6030            && self.config.write_formula_overlay_enabled)
6031        {
6032            return;
6033        }
6034        if self.computed_overlay_mirroring_disabled {
6035            return;
6036        }
6037
6038        self.ensure_arrow_sheet(sheet);
6039
6040        let row0 = row0 as usize;
6041        let col0 = col0 as usize;
6042        let asheet = self
6043            .arrow_sheets
6044            .sheet_mut(sheet)
6045            .expect("ArrowSheet must exist");
6046
6047        let cur_cols = asheet.columns.len();
6048        if col0 >= cur_cols {
6049            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
6050        }
6051
6052        if row0 >= asheet.nrows as usize {
6053            if asheet.columns.is_empty() {
6054                asheet.insert_columns(0, 1);
6055            }
6056            asheet.ensure_row_capacity(row0 + 1);
6057        }
6058
6059        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
6060            return;
6061        };
6062        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
6063            return;
6064        };
6065
6066        let delta = ch.computed_overlay.set_scalar(in_off, value);
6067        self.adjust_computed_overlay_bytes(delta);
6068
6069        if let Some(cap) = self.config.max_overlay_memory_bytes
6070            && self.computed_overlay_bytes_estimate > cap
6071        {
6072            self.disable_computed_overlay_mirroring_due_to_budget(cap);
6073        }
6074    }
6075
6076    pub(crate) fn plan_computed_write_coalescing(
6077        &self,
6078        buffer: &ComputedWriteBuffer,
6079    ) -> ComputedWriteCoalescingPlan {
6080        self.plan_computed_write_coalescing_from_writes(buffer.writes().iter().cloned())
6081    }
6082
6083    fn plan_owned_computed_write_coalescing(
6084        &self,
6085        writes: Vec<ComputedWrite>,
6086    ) -> ComputedWriteCoalescingPlan {
6087        self.plan_computed_write_coalescing_from_writes(writes)
6088    }
6089
6090    fn plan_computed_write_coalescing_from_writes(
6091        &self,
6092        writes: impl IntoIterator<Item = ComputedWrite>,
6093    ) -> ComputedWriteCoalescingPlan {
6094        let mut groups: BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>> =
6095            BTreeMap::new();
6096        let mut input_cells = 0usize;
6097
6098        for write in writes {
6099            match write {
6100                ComputedWrite::Cell {
6101                    seq,
6102                    sheet_id,
6103                    row0,
6104                    col0,
6105                    value,
6106                } => {
6107                    input_cells = input_cells.saturating_add(1);
6108                    self.push_computed_write_plan_entry(
6109                        &mut groups,
6110                        seq,
6111                        sheet_id,
6112                        row0,
6113                        col0,
6114                        value,
6115                    );
6116                }
6117                ComputedWrite::Rect {
6118                    seq,
6119                    sheet_id,
6120                    sr0,
6121                    sc0,
6122                    values,
6123                } => {
6124                    for (r_off, row) in values.into_iter().enumerate() {
6125                        for (c_off, value) in row.into_iter().enumerate() {
6126                            input_cells = input_cells.saturating_add(1);
6127                            self.push_computed_write_plan_entry(
6128                                &mut groups,
6129                                seq,
6130                                sheet_id,
6131                                sr0.saturating_add(r_off as u32),
6132                                sc0.saturating_add(c_off as u32),
6133                                value,
6134                            );
6135                        }
6136                    }
6137                }
6138            }
6139        }
6140
6141        let mut plan = ComputedWriteCoalescingPlan {
6142            chunks: Vec::with_capacity(groups.len()),
6143            input_cells,
6144            coalesced_cells: 0,
6145            overwritten_cells: 0,
6146        };
6147        for (key, entries) in groups {
6148            let (chunk_plan, overwritten) = ComputedWriteChunkPlan::from_group(key, entries);
6149            plan.coalesced_cells = plan
6150                .coalesced_cells
6151                .saturating_add(chunk_plan.entries.len());
6152            plan.overwritten_cells = plan.overwritten_cells.saturating_add(overwritten);
6153            plan.chunks.push(chunk_plan);
6154        }
6155        debug_assert_eq!(
6156            plan.input_cells,
6157            plan.coalesced_cells.saturating_add(plan.overwritten_cells)
6158        );
6159        plan
6160    }
6161
6162    fn push_computed_write_plan_entry(
6163        &self,
6164        groups: &mut BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>>,
6165        seq: u64,
6166        sheet_id: SheetId,
6167        row0: u32,
6168        col0: u32,
6169        value: OverlayValue,
6170    ) {
6171        let (chunk_idx, chunk_start_row0, row_in_chunk) =
6172            self.locate_computed_write_chunk(sheet_id, row0);
6173        let key = ComputedWriteChunkKey {
6174            sheet_id,
6175            col0,
6176            chunk_idx,
6177            chunk_start_row0,
6178        };
6179        groups
6180            .entry(key)
6181            .or_default()
6182            .push(ComputedWriteChunkEntryPlan {
6183                row_in_chunk,
6184                seq,
6185                value,
6186            });
6187    }
6188
6189    fn locate_computed_write_chunk(&self, sheet_id: SheetId, row0: u32) -> (usize, u32, usize) {
6190        let sheet_name = self.graph.sheet_name(sheet_id);
6191        if let Some(sheet) = self.arrow_sheets.sheet(sheet_name) {
6192            return Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize);
6193        }
6194        Self::locate_row_in_empty_sheet_for_computed_write_plan(row0 as usize, 32 * 1024)
6195    }
6196
6197    fn locate_row_in_sheet_for_computed_write_plan(
6198        sheet: &crate::arrow_store::ArrowSheet,
6199        row0: usize,
6200    ) -> (usize, u32, usize) {
6201        if row0 < sheet.nrows as usize
6202            && let Some((chunk_idx, row_in_chunk)) = sheet.chunk_of_row(row0)
6203        {
6204            let chunk_start = sheet.chunk_starts.get(chunk_idx).copied().unwrap_or(0);
6205            return (chunk_idx, chunk_start as u32, row_in_chunk);
6206        }
6207
6208        let chunk_rows = sheet.chunk_rows.max(1);
6209        if sheet.chunk_starts.is_empty() {
6210            return Self::locate_row_in_empty_sheet_for_computed_write_plan(row0, chunk_rows);
6211        }
6212
6213        let mut chunk_idx = sheet.chunk_starts.len().saturating_sub(1);
6214        let mut chunk_start = sheet.chunk_starts[chunk_idx];
6215        while chunk_start.saturating_add(chunk_rows) <= row0 {
6216            chunk_idx = chunk_idx.saturating_add(1);
6217            chunk_start = chunk_start.saturating_add(chunk_rows);
6218        }
6219        (
6220            chunk_idx,
6221            chunk_start as u32,
6222            row0.saturating_sub(chunk_start),
6223        )
6224    }
6225
6226    fn locate_row_in_empty_sheet_for_computed_write_plan(
6227        row0: usize,
6228        chunk_rows: usize,
6229    ) -> (usize, u32, usize) {
6230        let chunk_rows = chunk_rows.max(1);
6231        let chunk_idx = row0 / chunk_rows;
6232        let chunk_start = chunk_idx.saturating_mul(chunk_rows);
6233        (
6234            chunk_idx,
6235            chunk_start as u32,
6236            row0.saturating_sub(chunk_start),
6237        )
6238    }
6239
6240    #[cfg(test)]
6241    pub(crate) fn debug_plan_computed_write_coalescing(
6242        &self,
6243        buffer: &ComputedWriteBuffer,
6244    ) -> ComputedWriteCoalescingPlan {
6245        self.plan_computed_write_coalescing(buffer)
6246    }
6247
6248    pub(crate) fn flush_computed_write_buffer(
6249        &mut self,
6250        buffer: &mut ComputedWriteBuffer,
6251    ) -> Result<(), ExcelError> {
6252        if buffer.is_empty() {
6253            return Ok(());
6254        }
6255
6256        let plan = self.plan_owned_computed_write_coalescing(buffer.take_writes());
6257        self.flush_computed_write_plan(plan);
6258
6259        Ok(())
6260    }
6261
6262    fn flush_computed_write_plan(&mut self, plan: ComputedWriteCoalescingPlan) {
6263        for chunk in plan.chunks {
6264            self.flush_computed_write_chunk_plan(chunk);
6265        }
6266    }
6267
6268    fn flush_computed_write_chunk_plan(&mut self, chunk: ComputedWriteChunkPlan) {
6269        match &chunk.shape {
6270            ComputedWriteChunkPlanShape::Point => {
6271                self.flush_computed_write_chunk_plan_as_points(chunk);
6272            }
6273            ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
6274                self.flush_computed_write_chunk_plan_as_sparse_fragment_or_points(chunk);
6275            }
6276            ComputedWriteChunkPlanShape::DenseRange { .. } => {
6277                self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
6278            }
6279            ComputedWriteChunkPlanShape::RunRange { len, runs, .. } => {
6280                if Self::should_emit_computed_run_fragment(*len, *runs) {
6281                    self.flush_computed_write_chunk_plan_as_run_fragment(chunk);
6282                } else {
6283                    self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
6284                }
6285            }
6286        }
6287    }
6288
6289    #[inline]
6290    fn should_emit_computed_run_fragment(len: usize, runs: usize) -> bool {
6291        runs <= len / 2
6292    }
6293
6294    fn flush_computed_write_chunk_plan_as_points(&mut self, chunk: ComputedWriteChunkPlan) {
6295        let sheet_name = self.graph.sheet_name(chunk.sheet_id).to_string();
6296        for entry in chunk.entries {
6297            let row0 = chunk
6298                .chunk_start_row0
6299                .saturating_add(entry.row_in_chunk as u32);
6300            self.write_computed_overlay_value_0based(&sheet_name, row0, chunk.col0, entry.value);
6301        }
6302    }
6303
6304    fn flush_computed_write_chunk_plan_as_sparse_fragment_or_points(
6305        &mut self,
6306        chunk: ComputedWriteChunkPlan,
6307    ) {
6308        let point_estimate = Self::computed_write_chunk_plan_point_estimate(&chunk);
6309        let sheet_id = chunk.sheet_id;
6310        let col0 = chunk.col0;
6311        let chunk_idx = chunk.chunk_idx;
6312        let chunk_start_row0 = chunk.chunk_start_row0;
6313        let items: Vec<(usize, OverlayValue)> = chunk
6314            .entries
6315            .into_iter()
6316            .map(|entry| (entry.row_in_chunk, entry.value))
6317            .collect();
6318        match OverlayFragment::sparse_offsets_if_estimated_smaller_than_points(
6319            items,
6320            point_estimate,
6321        ) {
6322            Some(Ok(fragment)) => {
6323                self.apply_computed_overlay_fragment(sheet_id, col0, chunk_idx, fragment);
6324            }
6325            Some(Err(cells)) => {
6326                self.flush_computed_overlay_cells_as_points(
6327                    sheet_id,
6328                    col0,
6329                    chunk_start_row0,
6330                    cells,
6331                );
6332            }
6333            None => {}
6334        }
6335    }
6336
6337    #[inline]
6338    fn computed_write_chunk_plan_point_estimate(chunk: &ComputedWriteChunkPlan) -> usize {
6339        chunk
6340            .entries
6341            .iter()
6342            .map(|entry| ComputedWriteBuffer::estimate_value_bytes(&entry.value))
6343            .fold(0usize, usize::saturating_add)
6344    }
6345
6346    fn flush_computed_overlay_cells_as_points(
6347        &mut self,
6348        sheet_id: SheetId,
6349        col0: u32,
6350        chunk_start_row0: u32,
6351        cells: Vec<(usize, OverlayValue)>,
6352    ) {
6353        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
6354        for (row_in_chunk, value) in cells {
6355            let row0 = chunk_start_row0.saturating_add(row_in_chunk as u32);
6356            self.write_computed_overlay_value_0based(&sheet_name, row0, col0, value);
6357        }
6358    }
6359
6360    fn flush_computed_write_chunk_plan_as_dense_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
6361        if chunk.entries.is_empty() {
6362            return;
6363        }
6364        let start = chunk.entries[0].row_in_chunk;
6365        let values: Vec<OverlayValue> =
6366            chunk.entries.into_iter().map(|entry| entry.value).collect();
6367        if let Some(fragment) = OverlayFragment::dense_range(start, values) {
6368            self.apply_computed_overlay_fragment(
6369                chunk.sheet_id,
6370                chunk.col0,
6371                chunk.chunk_idx,
6372                fragment,
6373            );
6374        }
6375    }
6376
6377    fn flush_computed_write_chunk_plan_as_run_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
6378        if chunk.entries.is_empty() {
6379            return;
6380        }
6381        let start = chunk.entries[0].row_in_chunk;
6382        let values: Vec<OverlayValue> =
6383            chunk.entries.into_iter().map(|entry| entry.value).collect();
6384        if let Some(fragment) = OverlayFragment::run_range(start, values) {
6385            self.apply_computed_overlay_fragment(
6386                chunk.sheet_id,
6387                chunk.col0,
6388                chunk.chunk_idx,
6389                fragment,
6390            );
6391        }
6392    }
6393
6394    fn apply_computed_overlay_fragment(
6395        &mut self,
6396        sheet_id: SheetId,
6397        col0: u32,
6398        chunk_idx: usize,
6399        fragment: OverlayFragment,
6400    ) {
6401        if !(self.config.arrow_storage_enabled
6402            && self.config.delta_overlay_enabled
6403            && self.config.write_formula_overlay_enabled)
6404        {
6405            return;
6406        }
6407        if self.computed_overlay_mirroring_disabled {
6408            return;
6409        }
6410
6411        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
6412        self.ensure_arrow_sheet(&sheet_name);
6413
6414        let col0 = col0 as usize;
6415        let asheet = self
6416            .arrow_sheets
6417            .sheet_mut(&sheet_name)
6418            .expect("ArrowSheet must exist");
6419
6420        let cur_cols = asheet.columns.len();
6421        if col0 >= cur_cols {
6422            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
6423        }
6424
6425        let start_row0 = asheet
6426            .chunk_starts
6427            .get(chunk_idx)
6428            .copied()
6429            .unwrap_or_else(|| chunk_idx.saturating_mul(asheet.chunk_rows.max(1)));
6430        let required_rows =
6431            start_row0.saturating_add(fragment.max_covered_offset().saturating_add(1));
6432        if required_rows > asheet.nrows as usize {
6433            if asheet.columns.is_empty() {
6434                asheet.insert_columns(0, 1);
6435            }
6436            asheet.ensure_row_capacity(required_rows);
6437        }
6438
6439        let Some(ch) = asheet.ensure_column_chunk_mut(col0, chunk_idx) else {
6440            return;
6441        };
6442        let delta = ch.computed_overlay.apply_fragment(fragment);
6443        self.adjust_computed_overlay_bytes(delta);
6444
6445        if let Some(cap) = self.config.max_overlay_memory_bytes
6446            && self.computed_overlay_bytes_estimate > cap
6447        {
6448            self.disable_computed_overlay_mirroring_due_to_budget(cap);
6449        }
6450    }
6451
6452    #[inline]
6453    fn adjust_computed_overlay_bytes(&mut self, delta: isize) {
6454        if delta >= 0 {
6455            self.computed_overlay_bytes_estimate = self
6456                .computed_overlay_bytes_estimate
6457                .saturating_add(delta as usize);
6458        } else {
6459            self.computed_overlay_bytes_estimate = self
6460                .computed_overlay_bytes_estimate
6461                .saturating_sub((-delta) as usize);
6462        }
6463    }
6464
6465    fn clear_all_computed_overlays(&mut self) {
6466        let mut freed_total = 0usize;
6467        for sh in self.arrow_sheets.sheets.iter_mut() {
6468            for col in sh.columns.iter_mut() {
6469                for ch in col.chunks.iter_mut() {
6470                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
6471                }
6472                for ch in col.sparse_chunks.values_mut() {
6473                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
6474                }
6475            }
6476        }
6477        self.computed_overlay_bytes_estimate = self
6478            .computed_overlay_bytes_estimate
6479            .saturating_sub(freed_total);
6480    }
6481
6482    fn disable_computed_overlay_mirroring_due_to_budget(&mut self, _cap: usize) {
6483        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
6484        // Handle budget pressure by compacting computed overlays into base lanes.
6485        self.compact_all_computed_overlays();
6486    }
6487
6488    /// Fold all computed overlay entries across all sheets into their base arrays.
6489    /// This preserves data while freeing overlay memory, allowing mirroring to continue.
6490    fn compact_all_computed_overlays(&mut self) {
6491        let mut freed_total = 0usize;
6492        for sheet in self.arrow_sheets.sheets.iter_mut() {
6493            for col_idx in 0..sheet.columns.len() {
6494                // Dense chunks
6495                let num_dense = sheet.columns[col_idx].chunks.len();
6496                for ch_idx in 0..num_dense {
6497                    freed_total += sheet.compact_computed_overlay_chunk(col_idx, ch_idx);
6498                }
6499                // Sparse chunks
6500                let sparse_keys: Vec<usize> = sheet.columns[col_idx]
6501                    .sparse_chunks
6502                    .keys()
6503                    .copied()
6504                    .collect();
6505                for ch_idx in sparse_keys {
6506                    freed_total += sheet.compact_computed_overlay_sparse_chunk(col_idx, ch_idx);
6507                }
6508            }
6509        }
6510        self.computed_overlay_bytes_estimate = self
6511            .computed_overlay_bytes_estimate
6512            .saturating_sub(freed_total);
6513        self.overlay_compactions = self.overlay_compactions.saturating_add(1);
6514    }
6515
6516    fn mirror_vertex_value_to_overlay(&mut self, vertex_id: VertexId, value: &LiteralValue) {
6517        let _ = self.record_vertex_value_to_overlay(vertex_id, value, None);
6518    }
6519
6520    fn record_vertex_value_to_overlay(
6521        &mut self,
6522        vertex_id: VertexId,
6523        value: &LiteralValue,
6524        computed_writes: Option<&mut ComputedWriteBuffer>,
6525    ) -> Result<(), ExcelError> {
6526        if !(self.config.arrow_storage_enabled
6527            && self.config.delta_overlay_enabled
6528            && self.config.write_formula_overlay_enabled)
6529        {
6530            return Ok(());
6531        }
6532        if self.computed_overlay_mirroring_disabled {
6533            return Ok(());
6534        }
6535        if !matches!(
6536            self.graph.get_vertex_kind(vertex_id),
6537            VertexKind::FormulaScalar | VertexKind::FormulaArray
6538        ) {
6539            return Ok(());
6540        }
6541        let Some(cell) = self.graph.get_cell_ref(vertex_id) else {
6542            return Ok(());
6543        };
6544        let ov = self.literal_to_overlay_value(value);
6545        if let Some(buffer) = computed_writes {
6546            buffer.push_cell(cell.sheet_id, cell.coord.row(), cell.coord.col(), ov);
6547            if self.should_flush_computed_write_buffer(buffer) {
6548                self.flush_computed_write_buffer(buffer)?;
6549            }
6550        } else {
6551            let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
6552            self.write_computed_overlay_value_0based(
6553                &sheet_name,
6554                cell.coord.row(),
6555                cell.coord.col(),
6556                ov,
6557            );
6558        }
6559        Ok(())
6560    }
6561
6562    #[inline]
6563    fn should_flush_computed_write_buffer(&self, buffer: &ComputedWriteBuffer) -> bool {
6564        self.config.max_overlay_memory_bytes.is_some_and(|cap| {
6565            if cap == 0 {
6566                return false;
6567            }
6568            self.computed_overlay_bytes_estimate
6569                .saturating_add(buffer.estimated_bytes())
6570                > cap
6571        })
6572    }
6573
6574    /// Estimated memory usage for computed overlays (formula/spill mirroring).
6575    pub fn overlay_memory_usage(&self) -> usize {
6576        self.computed_overlay_bytes_estimate
6577    }
6578
6579    #[cfg(test)]
6580    pub(crate) fn debug_overlay_compactions(&self) -> u64 {
6581        self.overlay_compactions
6582    }
6583
6584    #[cfg(test)]
6585    pub(crate) fn debug_recompute_computed_overlay_bytes(&mut self) -> usize {
6586        let mut total = 0usize;
6587        for sheet in &self.arrow_sheets.sheets {
6588            for column in &sheet.columns {
6589                for chunk in &column.chunks {
6590                    total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
6591                }
6592                for chunk in column.sparse_chunks.values() {
6593                    total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
6594                }
6595            }
6596        }
6597        self.computed_overlay_bytes_estimate = total;
6598        total
6599    }
6600
6601    fn resolve_sheet_locator_for_write(
6602        &mut self,
6603        loc: formualizer_common::SheetLocator<'_>,
6604        current_sheet: &str,
6605    ) -> Result<SheetId, ExcelError> {
6606        Ok(match loc {
6607            formualizer_common::SheetLocator::Id(id) => id,
6608            formualizer_common::SheetLocator::Name(name) => self.graph.sheet_id_mut(name.as_ref()),
6609            formualizer_common::SheetLocator::Current => self.graph.sheet_id_mut(current_sheet),
6610        })
6611    }
6612
6613    fn resolve_sheet_locator_for_read(
6614        &self,
6615        loc: formualizer_common::SheetLocator<'_>,
6616        current_sheet: &str,
6617    ) -> Result<SheetId, ExcelError> {
6618        match loc {
6619            formualizer_common::SheetLocator::Id(id) => Ok(id),
6620            formualizer_common::SheetLocator::Name(name) => self
6621                .graph
6622                .sheet_id(name.as_ref())
6623                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
6624            formualizer_common::SheetLocator::Current => self
6625                .graph
6626                .sheet_id(current_sheet)
6627                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
6628        }
6629    }
6630
6631    /// Set a cell value
6632    pub fn set_cell_value(
6633        &mut self,
6634        sheet: &str,
6635        row: u32,
6636        col: u32,
6637        value: LiteralValue,
6638    ) -> Result<(), ExcelError> {
6639        let sheet_id = self.graph.sheet_id_mut(sheet);
6640        self.demote_span_containing_cell_for_write(
6641            sheet_id,
6642            row.saturating_sub(1),
6643            col.saturating_sub(1),
6644        )
6645        .map_err(Self::editor_error_to_excel)?;
6646        self.graph.set_cell_value(sheet, row, col, value.clone())?;
6647        self.record_formula_plane_changed_cell(sheet, row, col);
6648        // Mirror into Arrow overlay when enabled
6649        self.mirror_value_to_overlay(sheet, row, col, &value);
6650        // Advance snapshot to reflect external mutation
6651        self.snapshot_id
6652            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6653        self.has_edited = true;
6654        Ok(())
6655    }
6656
6657    /// Record a single-cell change in FormulaPlane authority so the next
6658    /// `evaluate_all` under `AuthoritativeExperimental` can derive bounded
6659    /// span work from `FormulaConsumerReadIndex` instead of recomputing every
6660    /// active span.
6661    fn record_formula_plane_changed_cell(&mut self, sheet: &str, row: u32, col: u32) {
6662        if self.config.formula_plane_mode == FormulaPlaneMode::Off {
6663            return;
6664        }
6665        let sheet_id = self.graph.sheet_id_mut(sheet);
6666        self.record_formula_plane_structural_change(StructuralScope::Cell {
6667            sheet: sheet_id,
6668            row: row.saturating_sub(1),
6669            col: col.saturating_sub(1),
6670        });
6671    }
6672
6673    fn record_formula_plane_change_for_event(&mut self, event: &ChangeEvent) {
6674        if self.config.formula_plane_mode == FormulaPlaneMode::Off {
6675            return;
6676        }
6677
6678        match event {
6679            ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
6680                self.record_formula_plane_structural_change(StructuralScope::Cell {
6681                    sheet: addr.sheet_id,
6682                    row: addr.coord.row(),
6683                    col: addr.coord.col(),
6684                });
6685            }
6686            ChangeEvent::SpillCommitted { new, .. } => {
6687                if let Some(scope) = Self::formula_plane_region_from_cells(&new.target_cells) {
6688                    self.record_formula_plane_structural_change(scope);
6689                }
6690            }
6691            ChangeEvent::SpillCleared { old, .. } => {
6692                if let Some(scope) = Self::formula_plane_region_from_cells(&old.target_cells) {
6693                    self.record_formula_plane_structural_change(scope);
6694                }
6695            }
6696            ChangeEvent::DefineName { .. }
6697            | ChangeEvent::UpdateName { .. }
6698            | ChangeEvent::DeleteName { .. }
6699            | ChangeEvent::VertexMoved { .. }
6700            | ChangeEvent::FormulaAdjusted { .. }
6701            | ChangeEvent::NamedRangeAdjusted { .. } => {
6702                self.record_formula_plane_structural_change(StructuralScope::AllSheets);
6703            }
6704            ChangeEvent::SetRowVisibility { sheet_id, row0, .. } => {
6705                self.record_formula_plane_structural_change(StructuralScope::Region(
6706                    Region::whole_row(*sheet_id, *row0),
6707                ));
6708            }
6709            ChangeEvent::AddVertex { .. }
6710            | ChangeEvent::RemoveVertex { .. }
6711            | ChangeEvent::EdgeAdded { .. }
6712            | ChangeEvent::EdgeRemoved { .. }
6713            | ChangeEvent::CompoundStart { .. }
6714            | ChangeEvent::CompoundEnd { .. }
6715            | ChangeEvent::StagedFormulaStateChanged { .. } => {}
6716        }
6717    }
6718
6719    fn record_formula_plane_structural_change(&mut self, scope: StructuralScope) {
6720        if self.config.formula_plane_mode == FormulaPlaneMode::Off {
6721            return;
6722        }
6723
6724        match scope {
6725            StructuralScope::Cell { sheet, row, col } => {
6726                self.graph
6727                    .formula_authority_mut()
6728                    .record_changed_region(Region::point(sheet, row, col));
6729            }
6730            StructuralScope::Region(region) => {
6731                self.graph
6732                    .formula_authority_mut()
6733                    .record_changed_region(region);
6734            }
6735            StructuralScope::Sheet(sheet_id) => {
6736                self.graph
6737                    .formula_authority_mut()
6738                    .record_changed_region(Region::whole_sheet(sheet_id));
6739            }
6740            StructuralScope::RemovedSheet(sheet_id) => {
6741                let removed_refs = {
6742                    let authority = self.graph.formula_authority();
6743                    authority
6744                        .active_span_refs()
6745                        .into_iter()
6746                        .filter(|span_ref| {
6747                            authority
6748                                .plane
6749                                .spans
6750                                .get(*span_ref)
6751                                .map(|span| span.sheet_id == sheet_id)
6752                                .unwrap_or(false)
6753                        })
6754                        .collect::<Vec<_>>()
6755                };
6756
6757                let authority = self.graph.formula_authority_mut();
6758                for span_ref in removed_refs {
6759                    authority.plane.remove_span(span_ref);
6760                }
6761                authority.mark_all_active_spans_dirty();
6762                let _ = authority.rebuild_indexes();
6763            }
6764            StructuralScope::AllSheets => {
6765                let authority = self.graph.formula_authority_mut();
6766                authority.mark_all_active_spans_dirty();
6767                let _ = authority.rebuild_indexes();
6768            }
6769        }
6770    }
6771
6772    fn formula_plane_region_from_cells(cells: &[CellRef]) -> Option<StructuralScope> {
6773        let first = cells.first()?;
6774        let sheet_id = first.sheet_id;
6775        if cells.iter().any(|cell| cell.sheet_id != sheet_id) {
6776            return Some(StructuralScope::AllSheets);
6777        }
6778        let mut row_start = first.coord.row();
6779        let mut row_end = row_start;
6780        let mut col_start = first.coord.col();
6781        let mut col_end = col_start;
6782        for cell in cells.iter().skip(1) {
6783            row_start = row_start.min(cell.coord.row());
6784            row_end = row_end.max(cell.coord.row());
6785            col_start = col_start.min(cell.coord.col());
6786            col_end = col_end.max(cell.coord.col());
6787        }
6788        Some(StructuralScope::Region(Region::rect(
6789            sheet_id, row_start, row_end, col_start, col_end,
6790        )))
6791    }
6792
6793    pub fn set_cell_value_ref(
6794        &mut self,
6795        cell: formualizer_common::SheetCellRef<'_>,
6796        current_sheet: &str,
6797        value: LiteralValue,
6798    ) -> Result<(), ExcelError> {
6799        let owned = cell.into_owned();
6800        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
6801        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
6802        self.set_cell_value(
6803            &sheet_name,
6804            owned.coord.row() + 1,
6805            owned.coord.col() + 1,
6806            value,
6807        )
6808    }
6809
6810    pub fn set_cell_formula_ref(
6811        &mut self,
6812        cell: formualizer_common::SheetCellRef<'_>,
6813        current_sheet: &str,
6814        ast: ASTNode,
6815    ) -> Result<(), ExcelError> {
6816        let owned = cell.into_owned();
6817        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
6818        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
6819        self.set_cell_formula(
6820            &sheet_name,
6821            owned.coord.row() + 1,
6822            owned.coord.col() + 1,
6823            ast,
6824        )
6825    }
6826
6827    pub fn get_cell_value_ref(
6828        &self,
6829        cell: formualizer_common::SheetCellRef<'_>,
6830        current_sheet: &str,
6831    ) -> Result<Option<LiteralValue>, ExcelError> {
6832        let owned = cell.into_owned();
6833        let sheet_id = self.resolve_sheet_locator_for_read(owned.sheet, current_sheet)?;
6834        let sheet_name = self.graph.sheet_name(sheet_id);
6835        Ok(self.get_cell_value(sheet_name, owned.coord.row() + 1, owned.coord.col() + 1))
6836    }
6837
6838    pub fn resolve_range_view_sheet_ref<'c>(
6839        &'c self,
6840        r: &formualizer_common::SheetRef<'_>,
6841        current_sheet: &str,
6842    ) -> Result<RangeView<'c>, ExcelError> {
6843        use formualizer_common::SheetLocator;
6844
6845        let sheet_to_opt_name = |loc: SheetLocator<'_>| -> Result<Option<String>, ExcelError> {
6846            match loc {
6847                SheetLocator::Current => Ok(None),
6848                SheetLocator::Name(name) => Ok(Some(name.as_ref().to_string())),
6849                SheetLocator::Id(id) => Ok(Some(self.graph.sheet_name(id).to_string())),
6850            }
6851        };
6852
6853        let rt = match r {
6854            formualizer_common::SheetRef::Cell(cell) => ReferenceType::Cell {
6855                sheet: sheet_to_opt_name(cell.sheet.clone())?,
6856                row: cell.coord.row() + 1,
6857                col: cell.coord.col() + 1,
6858                row_abs: cell.coord.row_abs(),
6859                col_abs: cell.coord.col_abs(),
6860            },
6861            formualizer_common::SheetRef::Range(range) => ReferenceType::Range {
6862                sheet: sheet_to_opt_name(range.sheet.clone())?,
6863                start_row: range.start_row.map(|b| b.index + 1),
6864                start_col: range.start_col.map(|b| b.index + 1),
6865                end_row: range.end_row.map(|b| b.index + 1),
6866                end_col: range.end_col.map(|b| b.index + 1),
6867                start_row_abs: range.start_row.map(|b| b.abs).unwrap_or(false),
6868                start_col_abs: range.start_col.map(|b| b.abs).unwrap_or(false),
6869                end_row_abs: range.end_row.map(|b| b.abs).unwrap_or(false),
6870                end_col_abs: range.end_col.map(|b| b.abs).unwrap_or(false),
6871            },
6872        };
6873
6874        crate::traits::EvaluationContext::resolve_range_view(self, &rt, current_sheet)
6875    }
6876
6877    /// Set a cell formula
6878    pub fn set_cell_formula(
6879        &mut self,
6880        sheet: &str,
6881        row: u32,
6882        col: u32,
6883        ast: ASTNode,
6884    ) -> Result<(), ExcelError> {
6885        let sheet_id = self.graph.sheet_id_mut(sheet);
6886        self.demote_span_containing_cell_for_write(
6887            sheet_id,
6888            row.saturating_sub(1),
6889            col.saturating_sub(1),
6890        )
6891        .map_err(Self::editor_error_to_excel)?;
6892        let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
6893        let ingested = {
6894            let mut pipeline = self.ingest_pipeline();
6895            pipeline.ingest_formula(FormulaAstInput::Tree(ast), placement, None)?
6896        };
6897        self.graph.set_cell_formula_with_plan(
6898            sheet,
6899            row,
6900            col,
6901            ingested.ast_id,
6902            &ingested.dep_plan,
6903            ingested.dep_plan.volatile,
6904            ingested.dep_plan.dynamic,
6905        )?;
6906        self.record_formula_plane_changed_cell(sheet, row, col);
6907
6908        // If the cell previously held a user value in the delta overlay, it must not continue
6909        // to mask the formula result under Arrow-canonical reads (overlay precedence is
6910        // delta -> computed -> base). Remove the overlay entry instead of writing `Empty`,
6911        // because an explicit `Empty` overlay would still take precedence over computed values.
6912        self.clear_delta_overlay_cell(sheet, row, col);
6913
6914        // Advance snapshot to reflect external mutation
6915        self.mark_topology_edited();
6916        Ok(())
6917    }
6918
6919    /// Bulk set many formulas on a sheet. Skips per-cell snapshot bumping and minimizes edge rebuilds.
6920    pub fn bulk_set_formulas<I>(&mut self, sheet: &str, items: I) -> Result<usize, ExcelError>
6921    where
6922        I: IntoIterator<Item = (u32, u32, ASTNode)>,
6923    {
6924        let collected: Vec<(u32, u32, ASTNode)> = items.into_iter().collect();
6925        let edited_cells: Vec<(u32, u32)> = collected.iter().map(|(r, c, _)| (*r, *c)).collect();
6926        let sheet_id = self.graph.sheet_id_mut(sheet);
6927        let writes_inside_active_span = edited_cells.iter().any(|(row, col)| {
6928            let placement =
6929                PlacementCoord::new(sheet_id, row.saturating_sub(1), col.saturating_sub(1));
6930            self.graph
6931                .formula_authority()
6932                .plane
6933                .spans
6934                .find_at(placement)
6935                .is_some()
6936        });
6937        if writes_inside_active_span {
6938            self.demote_spans_preserving_computed_overlays(sheet_id, Region::whole_sheet(sheet_id))
6939                .map_err(Self::editor_error_to_excel)?;
6940        }
6941        let ingested = {
6942            let mut pipeline = self.ingest_pipeline();
6943            let inputs = collected.into_iter().map(|(row, col, ast)| {
6944                let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
6945                (FormulaAstInput::Tree(ast), placement, None)
6946            });
6947            pipeline.ingest_batch(inputs)?
6948        };
6949        let planned = ingested
6950            .into_iter()
6951            .map(|formula| {
6952                (
6953                    formula.placement.coord.row() + 1,
6954                    formula.placement.coord.col() + 1,
6955                    formula.ast_id,
6956                    formula.dep_plan,
6957                )
6958            })
6959            .collect();
6960        let n = self.graph.bulk_set_formulas_with_plans(sheet, planned)?;
6961        for (row, col) in edited_cells {
6962            self.record_formula_plane_changed_cell(sheet, row, col);
6963        }
6964        // Single topology bump after batch
6965        if n > 0 {
6966            self.mark_topology_edited();
6967        }
6968        Ok(n)
6969    }
6970
6971    #[inline]
6972    fn normalize_public_cell_read(v: LiteralValue) -> Option<LiteralValue> {
6973        match v {
6974            LiteralValue::Empty => None,
6975            LiteralValue::Int(i) => Some(LiteralValue::Number(i as f64)),
6976            other => Some(other),
6977        }
6978    }
6979
6980    /// Get a cell value
6981    pub fn get_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
6982        self.read_cell_value(sheet, row, col)
6983            .and_then(Self::normalize_public_cell_read)
6984    }
6985
6986    /// Unified internal read API for a single cell value (Arrow-truth).
6987    pub(crate) fn read_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
6988        let asheet = self.sheet_store().sheet(sheet)?;
6989        let r0 = row.saturating_sub(1) as usize;
6990        let c0 = col.saturating_sub(1) as usize;
6991        let v = asheet.get_cell_value(r0, c0);
6992        if matches!(v, LiteralValue::Empty) {
6993            None
6994        } else {
6995            Some(v)
6996        }
6997    }
6998
6999    /// Unified internal read API for a range of cell values (Arrow-truth).
7000    pub(crate) fn read_range_values(
7001        &self,
7002        sheet: &str,
7003        sr: u32,
7004        sc: u32,
7005        er: u32,
7006        ec: u32,
7007    ) -> RangeView<'_> {
7008        let Some(asheet) = self.sheet_store().sheet(sheet) else {
7009            return RangeView::from_owned_rows(Vec::new(), self.config.date_system);
7010        };
7011        if er < sr || ec < sc {
7012            return asheet.range_view(1, 1, 0, 0);
7013        }
7014        let sr0 = sr.saturating_sub(1) as usize;
7015        let sc0 = sc.saturating_sub(1) as usize;
7016        let er0 = er.saturating_sub(1) as usize;
7017        let ec0 = ec.saturating_sub(1) as usize;
7018        asheet.range_view(sr0, sc0, er0, ec0)
7019    }
7020
7021    /// Get formula AST (if any) and current stored value for a cell
7022    pub fn get_cell(
7023        &self,
7024        sheet: &str,
7025        row: u32,
7026        col: u32,
7027    ) -> Option<(Option<formualizer_parse::ASTNode>, Option<LiteralValue>)> {
7028        let v = self.get_cell_value(sheet, row, col);
7029        let sheet_id = self.graph.sheet_id(sheet)?;
7030        let coord = Coord::from_excel(row, col, true, true);
7031        let cell = CellRef::new(sheet_id, coord);
7032        if let Some(vid) = self.graph.get_vertex_for_cell(&cell) {
7033            let ast = self.graph.get_formula_id(vid).and_then(|ast_id| {
7034                self.graph
7035                    .data_store()
7036                    .retrieve_ast(ast_id, self.graph.sheet_reg())
7037            });
7038            return Some((ast, v));
7039        }
7040
7041        let placement =
7042            crate::formula_plane::runtime::PlacementCoord::new(sheet_id, coord.row(), coord.col());
7043        let handle = self
7044            .graph
7045            .formula_authority()
7046            .plane
7047            .resolve_formula_at(placement, None);
7048        let template_id = match handle.resolution {
7049            crate::formula_plane::runtime::FormulaResolution::SpanPlacement {
7050                template_id, ..
7051            } => Some(template_id),
7052            crate::formula_plane::runtime::FormulaResolution::Overlay(overlay_ref) => self
7053                .graph
7054                .formula_authority()
7055                .plane
7056                .formula_overlay
7057                .get(overlay_ref)
7058                .and_then(|overlay| match overlay.kind {
7059                    crate::formula_plane::runtime::FormulaOverlayEntryKind::FormulaOverride(
7060                        template_id,
7061                    ) => Some(template_id),
7062                    _ => None,
7063                }),
7064            _ => None,
7065        };
7066        let ast = template_id.and_then(|template_id| {
7067            let ast_id = self
7068                .graph
7069                .formula_authority()
7070                .plane
7071                .templates
7072                .get(template_id)?
7073                .ast_id;
7074            self.graph
7075                .data_store()
7076                .retrieve_ast(ast_id, self.graph.sheet_reg())
7077        });
7078        if let Some(ast) = ast {
7079            Some((Some(ast), v))
7080        } else if v.is_some() {
7081            Some((None, v))
7082        } else {
7083            None
7084        }
7085    }
7086
7087    /// Begin batch operations - defer CSR rebuilds for better performance
7088    pub fn begin_batch(&mut self) {
7089        self.graph.begin_batch();
7090    }
7091
7092    /// End batch operations and trigger CSR rebuild
7093    pub fn end_batch(&mut self) {
7094        self.graph.end_batch();
7095    }
7096
7097    /// Evaluate a single vertex.
7098    /// This is the core of the sequential evaluation logic for Milestone 3.1.
7099    #[inline]
7100    fn record_cell_if_changed(
7101        delta: &mut DeltaCollector,
7102        cell: &CellRef,
7103        old: &LiteralValue,
7104        new: &LiteralValue,
7105    ) {
7106        if old != new {
7107            delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
7108        }
7109    }
7110
7111    pub fn evaluate_vertex(&mut self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
7112        if self.graph.formula_authority().active_span_count() > 0 {
7113            let _ = self.evaluate_authoritative_formula_plane_all()?;
7114        }
7115        self.evaluate_vertex_impl(vertex_id, None)
7116    }
7117
7118    fn evaluate_vertex_impl(
7119        &mut self,
7120        vertex_id: VertexId,
7121        delta: Option<&mut DeltaCollector>,
7122    ) -> Result<LiteralValue, ExcelError> {
7123        let mut delta = delta;
7124        // Check if vertex exists
7125        if !self.graph.vertex_exists(vertex_id) {
7126            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
7127                .with_message(format!("Vertex not found: {vertex_id:?}")));
7128        }
7129
7130        // Get vertex kind and check if it needs evaluation
7131        let kind = self.graph.get_vertex_kind(vertex_id);
7132        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
7133
7134        let ast_id = match kind {
7135            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
7136                if let Some(ast_id) = self.graph.get_formula_id(vertex_id) {
7137                    ast_id
7138                } else {
7139                    return Ok(LiteralValue::Number(0.0));
7140                }
7141            }
7142            VertexKind::Empty | VertexKind::Cell => {
7143                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
7144                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
7145                    let row = cell_ref.coord.row() + 1;
7146                    let col = cell_ref.coord.col() + 1;
7147                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
7148                        return Ok(v);
7149                    }
7150                }
7151                return Ok(LiteralValue::Number(0.0));
7152            }
7153            VertexKind::NamedScalar => {
7154                let value = self.evaluate_named_scalar(vertex_id, sheet_id)?;
7155                return Ok(value);
7156            }
7157            VertexKind::NamedArray => {
7158                let value = self.evaluate_named_array(vertex_id, sheet_id)?;
7159                return Ok(value);
7160            }
7161            VertexKind::InfiniteRange
7162            | VertexKind::Range
7163            | VertexKind::External
7164            | VertexKind::Table => {
7165                // Not directly evaluatable here.
7166                return Ok(LiteralValue::Number(0.0));
7167            }
7168        };
7169
7170        // The interpreter uses a reference to the engine as the context.
7171        let sheet_name = self.graph.sheet_name(sheet_id);
7172        let cell_ref = self
7173            .graph
7174            .get_cell_ref(vertex_id)
7175            .expect("cell ref for vertex");
7176        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
7177
7178        let result =
7179            interpreter.evaluate_arena_ast(ast_id, self.graph.data_store(), self.graph.sheet_reg());
7180
7181        // If array result, perform spill from the anchor cell
7182        match result {
7183            Ok(cv) => {
7184                let result_literal = cv.into_literal();
7185                match result_literal {
7186                    LiteralValue::Array(rows) => {
7187                        // Update kind to FormulaArray for tracking
7188                        self.graph
7189                            .set_kind(vertex_id, crate::engine::vertex::VertexKind::FormulaArray);
7190                        // Build target cells rectangle starting from anchor
7191                        let anchor = self
7192                            .graph
7193                            .get_cell_ref(vertex_id)
7194                            .expect("cell ref for vertex");
7195                        let sheet_id = anchor.sheet_id;
7196                        let h = rows.len() as u32;
7197                        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
7198
7199                        // Hard cap to avoid vertex explosion from huge dynamic arrays.
7200                        let spill_cells = (h as u64).saturating_mul(w as u64);
7201                        if spill_cells > self.config.spill.max_spill_cells as u64 {
7202                            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
7203                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
7204                                .with_message("SpillTooLarge")
7205                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
7206                                    expected_rows: h,
7207                                    expected_cols: w,
7208                                });
7209                            let spill_val = LiteralValue::Error(spill_err.clone());
7210                            if let Some(d) = delta.as_deref_mut() {
7211                                let old = self
7212                                    .read_cell_value(
7213                                        self.graph.sheet_name(anchor.sheet_id),
7214                                        anchor.coord.row() + 1,
7215                                        anchor.coord.col() + 1,
7216                                    )
7217                                    .unwrap_or(LiteralValue::Empty);
7218                                if old != spill_val {
7219                                    d.record_cell(
7220                                        anchor.sheet_id,
7221                                        anchor.coord.row(),
7222                                        anchor.coord.col(),
7223                                    );
7224                                }
7225                            }
7226                            self.graph.update_vertex_value(vertex_id, spill_val.clone());
7227                            if self.config.arrow_storage_enabled
7228                                && self.config.delta_overlay_enabled
7229                                && self.config.write_formula_overlay_enabled
7230                            {
7231                                let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
7232                                self.mirror_value_to_computed_overlay(
7233                                    &sheet_name,
7234                                    anchor.coord.row() + 1,
7235                                    anchor.coord.col() + 1,
7236                                    &spill_val,
7237                                );
7238                            }
7239                            return Ok(spill_val);
7240                        }
7241                        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity)
7242                        const PACKED_MAX_ROW: u32 = 1_048_575; // 20-bit max
7243                        const PACKED_MAX_COL: u32 = 16_383; // 14-bit max
7244                        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
7245                        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
7246                        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
7247                            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
7248                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
7249                                .with_message("Spill exceeds sheet bounds")
7250                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
7251                                    expected_rows: h,
7252                                    expected_cols: w,
7253                                });
7254                            let spill_val = LiteralValue::Error(spill_err.clone());
7255                            if let Some(d) = delta.as_deref_mut() {
7256                                let old = self
7257                                    .read_cell_value(
7258                                        self.graph.sheet_name(anchor.sheet_id),
7259                                        anchor.coord.row() + 1,
7260                                        anchor.coord.col() + 1,
7261                                    )
7262                                    .unwrap_or(LiteralValue::Empty);
7263                                if old != spill_val {
7264                                    d.record_cell(
7265                                        anchor.sheet_id,
7266                                        anchor.coord.row(),
7267                                        anchor.coord.col(),
7268                                    );
7269                                }
7270                            }
7271                            self.graph.update_vertex_value(vertex_id, spill_val.clone());
7272                            if self.config.arrow_storage_enabled
7273                                && self.config.delta_overlay_enabled
7274                                && self.config.write_formula_overlay_enabled
7275                            {
7276                                let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
7277                                self.mirror_value_to_computed_overlay(
7278                                    &sheet_name,
7279                                    anchor.coord.row() + 1,
7280                                    anchor.coord.col() + 1,
7281                                    &spill_val,
7282                                );
7283                            }
7284                            return Ok(spill_val);
7285                        }
7286                        let mut targets = Vec::new();
7287                        for r in 0..h {
7288                            for c in 0..w {
7289                                targets.push(self.graph.make_cell_ref_internal(
7290                                    sheet_id,
7291                                    anchor.coord.row() + r,
7292                                    anchor.coord.col() + c,
7293                                ));
7294                            }
7295                        }
7296
7297                        // Plan spill via spill manager shim
7298                        match self.spill_mgr.reserve(
7299                            vertex_id,
7300                            anchor,
7301                            SpillShape { rows: h, cols: w },
7302                            SpillMeta {
7303                                epoch: self.recalc_epoch,
7304                                config: self.config.spill,
7305                            },
7306                        ) {
7307                            Ok(()) => {
7308                                // Commit: write values to grid
7309                                // Default conflict policy is Error + FirstWins; reserve() enforces in-flight locks
7310                                // and plan_spill_region enforces overlap with committed formulas/spills/values.
7311                                if let Err(e) = self.commit_spill_and_mirror(
7312                                    vertex_id,
7313                                    &targets,
7314                                    rows.clone(),
7315                                    delta.as_deref_mut(),
7316                                    None,
7317                                ) {
7318                                    // If commit fails, mark as error
7319                                    self.clear_spill_projection_and_mirror(
7320                                        vertex_id,
7321                                        delta.as_deref_mut(),
7322                                    );
7323                                    if let Some(d) = delta.as_deref_mut() {
7324                                        let old = self
7325                                            .read_cell_value(
7326                                                self.graph.sheet_name(anchor.sheet_id),
7327                                                anchor.coord.row() + 1,
7328                                                anchor.coord.col() + 1,
7329                                            )
7330                                            .unwrap_or(LiteralValue::Empty);
7331                                        let new = LiteralValue::Error(e.clone());
7332                                        if old != new {
7333                                            d.record_cell(
7334                                                anchor.sheet_id,
7335                                                anchor.coord.row(),
7336                                                anchor.coord.col(),
7337                                            );
7338                                        }
7339                                    }
7340                                    let err_val = LiteralValue::Error(e.clone());
7341                                    self.graph.update_vertex_value(vertex_id, err_val.clone());
7342                                    if self.config.arrow_storage_enabled
7343                                        && self.config.delta_overlay_enabled
7344                                        && self.config.write_formula_overlay_enabled
7345                                    {
7346                                        let sheet_name =
7347                                            self.graph.sheet_name(anchor.sheet_id).to_string();
7348                                        self.mirror_value_to_computed_overlay(
7349                                            &sheet_name,
7350                                            anchor.coord.row() + 1,
7351                                            anchor.coord.col() + 1,
7352                                            &err_val,
7353                                        );
7354                                    }
7355                                    return Ok(err_val);
7356                                }
7357                                // Anchor shows the top-left value, like Excel
7358                                let top_left = rows
7359                                    .first()
7360                                    .and_then(|r| r.first())
7361                                    .cloned()
7362                                    .unwrap_or(LiteralValue::Empty);
7363                                self.graph.update_vertex_value(vertex_id, top_left.clone());
7364                                Ok(top_left)
7365                            }
7366                            Err(e) => {
7367                                self.clear_spill_projection_and_mirror(
7368                                    vertex_id,
7369                                    delta.as_deref_mut(),
7370                                );
7371                                let spill_err = ExcelError::new(ExcelErrorKind::Spill)
7372                                    .with_message(
7373                                        e.message.unwrap_or_else(|| "Spill blocked".to_string()),
7374                                    )
7375                                    .with_extra(formualizer_common::ExcelErrorExtra::Spill {
7376                                        expected_rows: h,
7377                                        expected_cols: w,
7378                                    });
7379                                let spill_val = LiteralValue::Error(spill_err.clone());
7380                                if let Some(d) = delta.as_deref_mut() {
7381                                    let old = self
7382                                        .read_cell_value(
7383                                            self.graph.sheet_name(anchor.sheet_id),
7384                                            anchor.coord.row() + 1,
7385                                            anchor.coord.col() + 1,
7386                                        )
7387                                        .unwrap_or(LiteralValue::Empty);
7388                                    if old != spill_val {
7389                                        d.record_cell(
7390                                            anchor.sheet_id,
7391                                            anchor.coord.row(),
7392                                            anchor.coord.col(),
7393                                        );
7394                                    }
7395                                }
7396                                self.graph.update_vertex_value(vertex_id, spill_val.clone());
7397                                if self.config.arrow_storage_enabled
7398                                    && self.config.delta_overlay_enabled
7399                                    && self.config.write_formula_overlay_enabled
7400                                {
7401                                    let sheet_name =
7402                                        self.graph.sheet_name(anchor.sheet_id).to_string();
7403                                    self.mirror_value_to_computed_overlay(
7404                                        &sheet_name,
7405                                        anchor.coord.row() + 1,
7406                                        anchor.coord.col() + 1,
7407                                        &spill_val,
7408                                    );
7409                                }
7410                                Ok(spill_val)
7411                            }
7412                        }
7413                    }
7414                    other => {
7415                        // Scalar result: store value and ensure any previous spill is cleared
7416                        let spill_cells = self
7417                            .graph
7418                            .spill_cells_for_anchor(vertex_id)
7419                            .map(|cells| cells.to_vec())
7420                            .unwrap_or_default();
7421                        if let Some(d) = delta.as_deref_mut()
7422                            && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
7423                        {
7424                            if spill_cells.is_empty() {
7425                                let old = self
7426                                    .read_cell_value(
7427                                        self.graph.sheet_name(anchor.sheet_id),
7428                                        anchor.coord.row() + 1,
7429                                        anchor.coord.col() + 1,
7430                                    )
7431                                    .unwrap_or(LiteralValue::Empty);
7432                                if old != other {
7433                                    d.record_cell(
7434                                        anchor.sheet_id,
7435                                        anchor.coord.row(),
7436                                        anchor.coord.col(),
7437                                    );
7438                                }
7439                            } else {
7440                                for cell in spill_cells.iter() {
7441                                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
7442                                    let old = self
7443                                        .get_cell_value(
7444                                            sheet_name,
7445                                            cell.coord.row() + 1,
7446                                            cell.coord.col() + 1,
7447                                        )
7448                                        .unwrap_or(LiteralValue::Empty);
7449                                    let new = if cell.sheet_id == anchor.sheet_id
7450                                        && cell.coord.row() == anchor.coord.row()
7451                                        && cell.coord.col() == anchor.coord.col()
7452                                    {
7453                                        other.clone()
7454                                    } else {
7455                                        LiteralValue::Empty
7456                                    };
7457                                    Self::record_cell_if_changed(d, cell, &old, &new);
7458                                }
7459                            }
7460                        }
7461                        self.graph.clear_spill_region(vertex_id);
7462                        if let Some(scope) = Self::formula_plane_region_from_cells(&spill_cells) {
7463                            self.record_formula_plane_structural_change(scope);
7464                        }
7465                        if self.config.arrow_storage_enabled
7466                            && self.config.delta_overlay_enabled
7467                            && self.config.write_formula_overlay_enabled
7468                        {
7469                            let empty = LiteralValue::Empty;
7470                            for cell in spill_cells.iter() {
7471                                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
7472                                self.mirror_value_to_computed_overlay(
7473                                    &sheet_name,
7474                                    cell.coord.row() + 1,
7475                                    cell.coord.col() + 1,
7476                                    &empty,
7477                                );
7478                            }
7479                        }
7480                        self.graph.update_vertex_value(vertex_id, other.clone());
7481                        // Optionally mirror into Arrow overlay for Arrow-backed reads
7482                        if self.config.arrow_storage_enabled
7483                            && self.config.delta_overlay_enabled
7484                            && self.config.write_formula_overlay_enabled
7485                        {
7486                            let anchor = self
7487                                .graph
7488                                .get_cell_ref(vertex_id)
7489                                .expect("cell ref for vertex");
7490                            let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
7491                            self.mirror_value_to_computed_overlay(
7492                                &sheet_name,
7493                                anchor.coord.row() + 1,
7494                                anchor.coord.col() + 1,
7495                                &other,
7496                            );
7497                        }
7498                        Ok(other)
7499                    }
7500                }
7501            }
7502            Err(e) => {
7503                // Runtime Excel error: store as a cell value instead of propagating
7504                // as an exception so bulk eval paths don't fail the whole pass.
7505                let spill_cells = self
7506                    .graph
7507                    .spill_cells_for_anchor(vertex_id)
7508                    .map(|cells| cells.to_vec())
7509                    .unwrap_or_default();
7510                let err_val = LiteralValue::Error(e.clone());
7511                if let Some(d) = delta
7512                    && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
7513                {
7514                    if spill_cells.is_empty() {
7515                        let old = self
7516                            .read_cell_value(
7517                                self.graph.sheet_name(anchor.sheet_id),
7518                                anchor.coord.row() + 1,
7519                                anchor.coord.col() + 1,
7520                            )
7521                            .unwrap_or(LiteralValue::Empty);
7522                        if old != err_val {
7523                            d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
7524                        }
7525                    } else {
7526                        for cell in spill_cells.iter() {
7527                            let sheet_name = self.graph.sheet_name(cell.sheet_id);
7528                            let old = self
7529                                .get_cell_value(
7530                                    sheet_name,
7531                                    cell.coord.row() + 1,
7532                                    cell.coord.col() + 1,
7533                                )
7534                                .unwrap_or(LiteralValue::Empty);
7535                            let new = if cell.sheet_id == anchor.sheet_id
7536                                && cell.coord.row() == anchor.coord.row()
7537                                && cell.coord.col() == anchor.coord.col()
7538                            {
7539                                err_val.clone()
7540                            } else {
7541                                LiteralValue::Empty
7542                            };
7543                            Self::record_cell_if_changed(d, cell, &old, &new);
7544                        }
7545                    }
7546                }
7547                self.graph.clear_spill_region(vertex_id);
7548                if let Some(scope) = Self::formula_plane_region_from_cells(&spill_cells) {
7549                    self.record_formula_plane_structural_change(scope);
7550                }
7551                if self.config.arrow_storage_enabled
7552                    && self.config.delta_overlay_enabled
7553                    && self.config.write_formula_overlay_enabled
7554                {
7555                    let empty = LiteralValue::Empty;
7556                    for cell in spill_cells.iter() {
7557                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
7558                        self.mirror_value_to_computed_overlay(
7559                            &sheet_name,
7560                            cell.coord.row() + 1,
7561                            cell.coord.col() + 1,
7562                            &empty,
7563                        );
7564                    }
7565                }
7566                self.graph.update_vertex_value(vertex_id, err_val.clone());
7567                if self.config.arrow_storage_enabled
7568                    && self.config.delta_overlay_enabled
7569                    && self.config.write_formula_overlay_enabled
7570                {
7571                    let anchor = self
7572                        .graph
7573                        .get_cell_ref(vertex_id)
7574                        .expect("cell ref for vertex");
7575                    let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
7576                    self.mirror_value_to_computed_overlay(
7577                        &sheet_name,
7578                        anchor.coord.row() + 1,
7579                        anchor.coord.col() + 1,
7580                        &err_val,
7581                    );
7582                }
7583                Ok(err_val)
7584            }
7585        }
7586    }
7587
7588    fn evaluate_named_scalar(
7589        &mut self,
7590        vertex_id: VertexId,
7591        sheet_id: SheetId,
7592    ) -> Result<LiteralValue, ExcelError> {
7593        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
7594            ExcelError::new(ExcelErrorKind::Name)
7595                .with_message("Named range metadata missing".to_string())
7596        })?;
7597
7598        match &named_range.definition {
7599            NamedDefinition::Cell(cell_ref) => {
7600                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
7601                let row = cell_ref.coord.row() + 1;
7602                let col = cell_ref.coord.col() + 1;
7603
7604                if let Some(dep_vertex) = self.graph.get_vertex_for_cell(cell_ref)
7605                    && matches!(
7606                        self.graph.get_vertex_kind(dep_vertex),
7607                        VertexKind::FormulaScalar | VertexKind::FormulaArray
7608                    )
7609                {
7610                    // Graph does not cache cell/formula values; ensure the precedent is evaluated.
7611                    let value = self.evaluate_vertex(dep_vertex)?;
7612                    self.graph.update_vertex_value(vertex_id, value.clone());
7613                    Ok(value)
7614                } else {
7615                    let value = self
7616                        .get_cell_value(sheet_name, row, col)
7617                        .unwrap_or(LiteralValue::Empty);
7618                    self.graph.update_vertex_value(vertex_id, value.clone());
7619                    Ok(value)
7620                }
7621            }
7622            NamedDefinition::Literal(v) => {
7623                let out = v.clone();
7624                self.graph.update_vertex_value(vertex_id, out.clone());
7625                Ok(out)
7626            }
7627            NamedDefinition::Formula { ast, .. } => {
7628                let context_sheet = match named_range.scope {
7629                    NameScope::Sheet(id) => id,
7630                    NameScope::Workbook => sheet_id,
7631                };
7632                let sheet_name = self.graph.sheet_name(context_sheet);
7633                let cell_ref = self
7634                    .graph
7635                    .get_cell_ref(vertex_id)
7636                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
7637                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
7638                match interpreter.evaluate_ast(ast) {
7639                    Ok(cv) => {
7640                        let value = cv.into_literal();
7641                        match value {
7642                            LiteralValue::Array(_) => {
7643                                let err = ExcelError::new(ExcelErrorKind::NImpl)
7644                                    .with_message("Array result in scalar named range".to_string());
7645                                let err_val = LiteralValue::Error(err.clone());
7646                                self.graph.update_vertex_value(vertex_id, err_val.clone());
7647                                Ok(err_val)
7648                            }
7649                            other => {
7650                                self.graph.update_vertex_value(vertex_id, other.clone());
7651                                Ok(other)
7652                            }
7653                        }
7654                    }
7655                    Err(err) => {
7656                        let err_val = LiteralValue::Error(err.clone());
7657                        self.graph.update_vertex_value(vertex_id, err_val.clone());
7658                        Ok(err_val)
7659                    }
7660                }
7661            }
7662            NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
7663                .with_message("Range-valued name evaluated as scalar".to_string())),
7664        }
7665    }
7666
7667    fn evaluate_named_array(
7668        &mut self,
7669        vertex_id: VertexId,
7670        sheet_id: SheetId,
7671    ) -> Result<LiteralValue, ExcelError> {
7672        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
7673            ExcelError::new(ExcelErrorKind::Name)
7674                .with_message("Named range metadata missing".to_string())
7675        })?;
7676
7677        let out = match &named_range.definition {
7678            NamedDefinition::Range(range_ref) => {
7679                if range_ref.start.sheet_id != range_ref.end.sheet_id {
7680                    return Err(ExcelError::new(ExcelErrorKind::Ref)
7681                        .with_message("Named range cannot span sheets".to_string()));
7682                }
7683
7684                let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
7685                let sr0 = range_ref.start.coord.row();
7686                let sc0 = range_ref.start.coord.col();
7687                let er0 = range_ref.end.coord.row();
7688                let ec0 = range_ref.end.coord.col();
7689                if sr0 > er0 || sc0 > ec0 {
7690                    return Err(ExcelError::new(ExcelErrorKind::Ref)
7691                        .with_message("Invalid named range bounds".to_string()));
7692                }
7693
7694                let h = (er0 - sr0 + 1) as usize;
7695                let w = (ec0 - sc0 + 1) as usize;
7696                let cell_count = (h as u64).saturating_mul(w as u64);
7697                if cell_count > self.config.spill.max_spill_cells as u64 {
7698                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
7699                        "Named range too large to materialize as an array".to_string(),
7700                    ));
7701                }
7702
7703                let mut rows = Vec::with_capacity(h);
7704                for r0 in sr0..=er0 {
7705                    let mut row = Vec::with_capacity(w);
7706                    for c0 in sc0..=ec0 {
7707                        let v = self
7708                            .get_cell_value(sheet_name, r0 + 1, c0 + 1)
7709                            .unwrap_or(LiteralValue::Empty);
7710                        row.push(v);
7711                    }
7712                    rows.push(row);
7713                }
7714                LiteralValue::Array(rows)
7715            }
7716            NamedDefinition::Cell(cell_ref) => {
7717                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
7718                let row = cell_ref.coord.row() + 1;
7719                let col = cell_ref.coord.col() + 1;
7720                let v = self
7721                    .get_cell_value(sheet_name, row, col)
7722                    .unwrap_or(LiteralValue::Empty);
7723                LiteralValue::Array(vec![vec![v]])
7724            }
7725            NamedDefinition::Literal(v) => LiteralValue::Array(vec![vec![v.clone()]]),
7726            NamedDefinition::Formula { ast, .. } => {
7727                let context_sheet = match named_range.scope {
7728                    NameScope::Sheet(id) => id,
7729                    NameScope::Workbook => sheet_id,
7730                };
7731                let sheet_name = self.graph.sheet_name(context_sheet);
7732                let cell_ref = self
7733                    .graph
7734                    .get_cell_ref(vertex_id)
7735                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
7736                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
7737                match interpreter.evaluate_ast(ast) {
7738                    Ok(cv) => {
7739                        let v = cv.into_literal();
7740                        match v {
7741                            LiteralValue::Array(_) => v,
7742                            other => LiteralValue::Array(vec![vec![other]]),
7743                        }
7744                    }
7745                    Err(err) => LiteralValue::Error(err),
7746                }
7747            }
7748        };
7749
7750        self.graph.update_vertex_value(vertex_id, out.clone());
7751        Ok(out)
7752    }
7753
7754    /// Evaluate only the necessary precedents for specific target cells (demand-driven)
7755    pub fn evaluate_until(
7756        &mut self,
7757        targets: &[(&str, u32, u32)],
7758    ) -> Result<EvalResult, ExcelError> {
7759        #[cfg(feature = "tracing")]
7760        let _span_eval = tracing::info_span!("evaluate_until", targets = targets.len()).entered();
7761        let start = crate::instant::FzInstant::now();
7762        let _source_cache = self.source_cache_session();
7763        if self.graph.formula_authority().active_span_count() > 0 {
7764            return self.evaluate_authoritative_formula_plane_all();
7765        }
7766
7767        // Parse target cell addresses
7768        let mut target_addrs = Vec::new();
7769        for (sheet, row, col) in targets {
7770            // For now, assume simple A1-style references on default sheet
7771            // TODO: Parse complex references with sheets
7772            let sheet_id = self.graph.sheet_id_mut(sheet);
7773            let coord = Coord::from_excel(*row, *col, true, true);
7774            target_addrs.push(CellRef::new(sheet_id, coord));
7775        }
7776
7777        // Find vertex IDs for targets
7778        let mut target_vertex_ids = Vec::new();
7779        for addr in &target_addrs {
7780            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
7781                target_vertex_ids.push(*vertex_id);
7782            }
7783        }
7784
7785        if target_vertex_ids.is_empty() {
7786            return Ok(EvalResult {
7787                computed_vertices: 0,
7788                cycle_errors: 0,
7789                elapsed: start.elapsed(),
7790            });
7791        }
7792
7793        // Build demand subgraph with virtual edges for compressed ranges
7794        #[cfg(feature = "tracing")]
7795        let _span_sub = tracing::info_span!("demand_subgraph_build").entered();
7796        let (precedents_to_eval, vdeps) = self.build_demand_subgraph(&target_vertex_ids);
7797        #[cfg(feature = "tracing")]
7798        drop(_span_sub);
7799
7800        if precedents_to_eval.is_empty() {
7801            return Ok(EvalResult {
7802                computed_vertices: 0,
7803                cycle_errors: 0,
7804                elapsed: start.elapsed(),
7805            });
7806        }
7807
7808        // Create schedule for the minimal subgraph, honoring virtual edges
7809        let scheduler = Scheduler::new(&self.graph);
7810        #[cfg(feature = "tracing")]
7811        let _span_sched =
7812            tracing::info_span!("schedule_build", vertices = precedents_to_eval.len()).entered();
7813        let schedule = scheduler.create_schedule_with_virtual(&precedents_to_eval, &vdeps)?;
7814        #[cfg(feature = "tracing")]
7815        drop(_span_sched);
7816
7817        // Handle cycles first
7818        let mut cycle_errors = 0;
7819        for cycle in &schedule.cycles {
7820            cycle_errors += 1;
7821            let circ_error = LiteralValue::Error(
7822                ExcelError::new(ExcelErrorKind::Circ)
7823                    .with_message("Circular dependency detected".to_string()),
7824            );
7825            for &vertex_id in cycle {
7826                self.graph
7827                    .update_vertex_value(vertex_id, circ_error.clone());
7828                self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
7829            }
7830        }
7831
7832        // Evaluate layers (parallel when enabled, mirroring evaluate_all)
7833        let mut computed_vertices = 0;
7834        for layer in &schedule.layers {
7835            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
7836                computed_vertices += self.evaluate_layer_parallel(layer)?;
7837            } else {
7838                computed_vertices += self.evaluate_layer_sequential(layer)?;
7839            }
7840        }
7841
7842        // Clear warmup context at end of evaluation
7843
7844        // Clear dirty flags for evaluated vertices
7845        self.graph.clear_dirty_flags(&precedents_to_eval);
7846
7847        // Re-dirty volatile vertices
7848        self.graph.redirty_volatiles();
7849
7850        Ok(EvalResult {
7851            computed_vertices,
7852            cycle_errors,
7853            elapsed: start.elapsed(),
7854        })
7855    }
7856
7857    fn evaluate_until_with_delta_collector(
7858        &mut self,
7859        targets: &[(&str, u32, u32)],
7860        delta: &mut DeltaCollector,
7861    ) -> Result<EvalResult, ExcelError> {
7862        #[cfg(feature = "tracing")]
7863        let _span_eval =
7864            tracing::info_span!("evaluate_until_with_delta", targets = targets.len()).entered();
7865        let start = crate::instant::FzInstant::now();
7866        let _source_cache = self.source_cache_session();
7867
7868        let mut target_addrs = Vec::new();
7869        for (sheet, row, col) in targets {
7870            let sheet_id = self.graph.sheet_id_mut(sheet);
7871            let coord = Coord::from_excel(*row, *col, true, true);
7872            target_addrs.push(CellRef::new(sheet_id, coord));
7873        }
7874
7875        let mut target_vertex_ids = Vec::new();
7876        for addr in &target_addrs {
7877            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
7878                target_vertex_ids.push(*vertex_id);
7879            }
7880        }
7881
7882        if target_vertex_ids.is_empty() {
7883            return Ok(EvalResult {
7884                computed_vertices: 0,
7885                cycle_errors: 0,
7886                elapsed: start.elapsed(),
7887            });
7888        }
7889
7890        let (precedents_to_eval, vdeps) = self.build_demand_subgraph(&target_vertex_ids);
7891
7892        if precedents_to_eval.is_empty() {
7893            return Ok(EvalResult {
7894                computed_vertices: 0,
7895                cycle_errors: 0,
7896                elapsed: start.elapsed(),
7897            });
7898        }
7899
7900        let scheduler = Scheduler::new(&self.graph);
7901        let schedule = scheduler.create_schedule_with_virtual(&precedents_to_eval, &vdeps)?;
7902
7903        let mut cycle_errors = 0;
7904        let circ_error = LiteralValue::Error(
7905            ExcelError::new(ExcelErrorKind::Circ)
7906                .with_message("Circular dependency detected".to_string()),
7907        );
7908        for cycle in &schedule.cycles {
7909            cycle_errors += 1;
7910            for &vertex_id in cycle {
7911                if delta.mode != DeltaMode::Off
7912                    && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
7913                {
7914                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
7915                    let old = self
7916                        .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
7917                        .unwrap_or(LiteralValue::Empty);
7918                    if old != circ_error {
7919                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
7920                    }
7921                }
7922                self.graph
7923                    .update_vertex_value(vertex_id, circ_error.clone());
7924                self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
7925            }
7926        }
7927
7928        let mut computed_vertices = 0;
7929        for layer in &schedule.layers {
7930            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
7931                computed_vertices += self.evaluate_layer_parallel_with_delta(layer, delta)?;
7932            } else {
7933                computed_vertices += self.evaluate_layer_sequential_with_delta(layer, delta)?;
7934            }
7935        }
7936
7937        self.graph.clear_dirty_flags(&precedents_to_eval);
7938        self.graph.redirty_volatiles();
7939
7940        Ok(EvalResult {
7941            computed_vertices,
7942            cycle_errors,
7943            elapsed: start.elapsed(),
7944        })
7945    }
7946
7947    /// Build a reusable evaluation plan that covers every formula vertex in the workbook.
7948    pub fn build_recalc_plan(&self) -> Result<RecalcPlan, ExcelError> {
7949        let mut vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
7950        vertices.sort_unstable();
7951        if vertices.is_empty() {
7952            return Ok(RecalcPlan {
7953                schedule: crate::engine::Schedule {
7954                    layers: Vec::new(),
7955                    cycles: Vec::new(),
7956                },
7957                has_dynamic_refs: false,
7958            });
7959        }
7960
7961        let has_dynamic_refs = vertices.iter().copied().any(|v| self.graph.is_dynamic(v));
7962        let (schedule, _, _) = self.create_evaluation_schedule_uncached(&vertices)?;
7963        Ok(RecalcPlan {
7964            schedule,
7965            has_dynamic_refs,
7966        })
7967    }
7968
7969    /// Evaluate using a previously constructed plan. This avoids rebuilding layer schedules for each run.
7970    pub fn evaluate_recalc_plan(&mut self, plan: &RecalcPlan) -> Result<EvalResult, ExcelError> {
7971        let _source_cache = self.source_cache_session();
7972        self.validate_deterministic_mode()?;
7973        if self.config.defer_graph_building {
7974            self.build_graph_all()?;
7975        }
7976        if self.graph.formula_authority().active_span_count() > 0 {
7977            return self.evaluate_authoritative_formula_plane_all();
7978        }
7979
7980        let start = crate::instant::FzInstant::now();
7981        let dirty_vertices = self.graph.get_evaluation_vertices();
7982        if dirty_vertices.is_empty() {
7983            return Ok(EvalResult {
7984                computed_vertices: 0,
7985                cycle_errors: 0,
7986                elapsed: start.elapsed(),
7987            });
7988        }
7989
7990        // Dynamic-reference formulas (INDIRECT/OFFSET-class) require per-pass virtual-dep
7991        // augmentation. Reuse the direct recalc flow to preserve semantic parity.
7992        if plan.has_dynamic_refs {
7993            self.virtual_dep_fallback_activations =
7994                self.virtual_dep_fallback_activations.saturating_add(1);
7995            return self.evaluate_all();
7996        }
7997
7998        let dirty_set: FxHashSet<VertexId> = dirty_vertices.iter().copied().collect();
7999        let mut computed_vertices = 0;
8000        let mut cycle_errors = 0;
8001
8002        if !plan.schedule.cycles.is_empty() {
8003            let circ_error = LiteralValue::Error(
8004                ExcelError::new(ExcelErrorKind::Circ)
8005                    .with_message("Circular dependency detected".to_string()),
8006            );
8007            for cycle in &plan.schedule.cycles {
8008                if !cycle.iter().any(|v| dirty_set.contains(v)) {
8009                    continue;
8010                }
8011                cycle_errors += 1;
8012                for &vertex_id in cycle {
8013                    if dirty_set.contains(&vertex_id) {
8014                        self.graph
8015                            .update_vertex_value(vertex_id, circ_error.clone());
8016                        self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
8017                    }
8018                }
8019            }
8020        }
8021
8022        for layer in &plan.schedule.layers {
8023            let work: Vec<VertexId> = layer
8024                .vertices
8025                .iter()
8026                .copied()
8027                .filter(|v| dirty_set.contains(v))
8028                .collect();
8029            if work.is_empty() {
8030                continue;
8031            }
8032            let temp_layer = crate::engine::scheduler::Layer { vertices: work };
8033            if self.thread_pool.is_some() && temp_layer.vertices.len() > 1 {
8034                computed_vertices += self.evaluate_layer_parallel(&temp_layer)?;
8035            } else {
8036                computed_vertices += self.evaluate_layer_sequential(&temp_layer)?;
8037            }
8038        }
8039
8040        self.graph.clear_dirty_flags(&dirty_vertices);
8041        self.graph.redirty_volatiles();
8042
8043        Ok(EvalResult {
8044            computed_vertices,
8045            cycle_errors,
8046            elapsed: start.elapsed(),
8047        })
8048    }
8049    fn evaluate_authoritative_formula_plane_all(&mut self) -> Result<EvalResult, ExcelError> {
8050        // The FormulaPlane coordinator is now selected by mode for evaluate_all.
8051        // SingletonUnique formulas intentionally remain legacy graph vertices;
8052        // when no spans are active, execute through the private legacy primitive
8053        // rather than the public legacy entry path.
8054        if self.graph.formula_authority().active_span_count() == 0 {
8055            #[cfg(test)]
8056            {
8057                self.last_formula_plane_span_eval_report = None;
8058            }
8059            return self.evaluate_all_legacy_impl();
8060        }
8061
8062        // Decide span work seeding strategy: any active span we have not yet
8063        // evaluated under the current authority indexes generation must run
8064        // whole; subsequent passes use bounded dirty closures derived from
8065        // captured changed regions.
8066        let current_indexes_epoch = self.graph.formula_authority().indexes_epoch();
8067        let span_seed_mode = if self.formula_plane_indexes_epoch_seen != current_indexes_epoch {
8068            SpanSeedMode::WholeAll
8069        } else {
8070            SpanSeedMode::DirtyClosure
8071        };
8072        // Take pending regions out of the authority so subsequent reschedules
8073        // start from a clean slate after a successful eval pass.
8074        let pending_changed_regions = self
8075            .graph
8076            .formula_authority_mut()
8077            .take_pending_changed_regions();
8078
8079        let start = crate::instant::FzInstant::now();
8080        let (schedule, span_refs_by_id, plane_epoch, legacy_vertices) =
8081            self.build_formula_plane_mixed_schedule(span_seed_mode, &pending_changed_regions)?;
8082
8083        if !schedule.is_authoritative_safe() {
8084            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(format!(
8085                "FormulaPlane mixed schedule is not authoritative-safe: {:?}",
8086                schedule.fallbacks
8087            )));
8088        }
8089
8090        let mut computed_vertices = 0usize;
8091        #[cfg(test)]
8092        {
8093            self.last_formula_plane_span_eval_report = None;
8094        }
8095        for layer in schedule.layers {
8096            let mut buffer = ComputedWriteBuffer::default();
8097            let mut sink = SpanComputedWriteSink::new(&mut buffer);
8098            let work_items = layer.work;
8099            let mut work_index = 0usize;
8100            while work_index < work_items.len() {
8101                match work_items[work_index].producer {
8102                    FormulaProducerId::Span(span_id) => {
8103                        let span_ref = *span_refs_by_id.get(&span_id).ok_or_else(|| {
8104                            ExcelError::new(ExcelErrorKind::NImpl)
8105                                .with_message("FormulaPlane schedule referenced a stale span")
8106                        })?;
8107                        let sheet_id = {
8108                            let authority = self.graph.formula_authority();
8109                            let span = authority.plane.spans.get(span_ref).ok_or_else(|| {
8110                                ExcelError::new(ExcelErrorKind::NImpl)
8111                                    .with_message("FormulaPlane schedule referenced a stale span")
8112                            })?;
8113                            span.sheet_id
8114                        };
8115                        let current_sheet = self.graph.sheet_name(sheet_id);
8116                        let authority = self.graph.formula_authority();
8117                        let evaluator = SpanEvaluator::new(
8118                            &authority.plane,
8119                            self,
8120                            current_sheet,
8121                            self.graph.data_store(),
8122                            self.graph.sheet_reg(),
8123                        );
8124                        #[cfg(test)]
8125                        let mut last_group_report = None;
8126                        while work_index < work_items.len() {
8127                            let FormulaProducerId::Span(group_span_id) =
8128                                work_items[work_index].producer
8129                            else {
8130                                break;
8131                            };
8132                            let group_span_ref =
8133                                *span_refs_by_id.get(&group_span_id).ok_or_else(|| {
8134                                    ExcelError::new(ExcelErrorKind::NImpl).with_message(
8135                                        "FormulaPlane schedule referenced a stale span",
8136                                    )
8137                                })?;
8138                            let group_sheet_id = {
8139                                let authority = self.graph.formula_authority();
8140                                let span =
8141                                    authority.plane.spans.get(group_span_ref).ok_or_else(|| {
8142                                        ExcelError::new(ExcelErrorKind::NImpl).with_message(
8143                                            "FormulaPlane schedule referenced a stale span",
8144                                        )
8145                                    })?;
8146                                span.sheet_id
8147                            };
8148                            if group_sheet_id != sheet_id {
8149                                break;
8150                            }
8151
8152                            let dirty = producer_dirty_to_span_dirty(
8153                                work_items[work_index].dirty.clone(),
8154                                group_span_ref,
8155                            );
8156                            let task = SpanEvalTask {
8157                                span: group_span_ref,
8158                                dirty,
8159                                plane_epoch,
8160                            };
8161                            let report =
8162                                evaluator.evaluate_task(&task, &mut sink).map_err(|err| {
8163                                    ExcelError::new(ExcelErrorKind::NImpl).with_message(format!(
8164                                        "FormulaPlane span evaluation failed: {err:?}"
8165                                    ))
8166                                })?;
8167                            #[cfg(test)]
8168                            {
8169                                last_group_report = Some(report.clone());
8170                            }
8171                            computed_vertices = computed_vertices
8172                                .saturating_add(report.span_eval_placement_count as usize);
8173                            work_index = work_index.saturating_add(1);
8174                        }
8175                        #[cfg(test)]
8176                        {
8177                            if let Some(report) = last_group_report {
8178                                self.last_formula_plane_span_eval_report = Some(report);
8179                            }
8180                        }
8181                    }
8182                    FormulaProducerId::Legacy(vertex_id) => {
8183                        let _ = self.evaluate_vertex_impl(vertex_id, None)?;
8184                        computed_vertices = computed_vertices.saturating_add(1);
8185                        work_index = work_index.saturating_add(1);
8186                    }
8187                }
8188            }
8189            self.flush_computed_write_buffer(&mut buffer)?;
8190        }
8191
8192        self.graph.clear_dirty_flags(&legacy_vertices);
8193        // Drop dirty flags on any newly-scheduled FP runtime cells whose graph
8194        // vertices weren't in the dirty subset (e.g. recently-introduced span
8195        // result cells); legacy clear_dirty_flags is safe over the full set.
8196        self.graph.redirty_volatiles();
8197        // Mark this indexes-epoch as fully evaluated so subsequent passes can
8198        // use bounded span dirty closures rather than whole-span work.
8199        self.formula_plane_indexes_epoch_seen = self.graph.formula_authority().indexes_epoch();
8200        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
8201        Ok(EvalResult {
8202            computed_vertices,
8203            cycle_errors: 0,
8204            elapsed: start.elapsed(),
8205        })
8206    }
8207
8208    fn build_formula_plane_mixed_schedule(
8209        &self,
8210        span_seed_mode: SpanSeedMode,
8211        pending_changed_regions: &[Region],
8212    ) -> Result<FormulaPlaneMixedScheduleBuild, ExcelError> {
8213        let authority = self.graph.formula_authority();
8214        let mut producer_results = FormulaProducerResultIndex::default();
8215        let mut consumer_reads = FormulaConsumerReadIndex::default();
8216        let mut work = Vec::new();
8217
8218        // Legacy formula producers participate in the mixed runtime only when
8219        // they are dirty under graph semantics. Result/read indexes still cover
8220        // every legacy formula so that span->legacy and legacy->span ordering is
8221        // visible to the scheduler regardless of dirty status, but only dirty
8222        // vertices receive scheduled work.
8223        let dirty_legacy: rustc_hash::FxHashSet<VertexId> =
8224            self.graph.get_evaluation_vertices().into_iter().collect();
8225
8226        let span_refs = authority.active_span_refs();
8227        let span_refs_by_id = span_refs
8228            .iter()
8229            .copied()
8230            .map(|span_ref| (span_ref.id, span_ref))
8231            .collect::<BTreeMap<_, _>>();
8232        for span_ref in &span_refs {
8233            let span = authority.plane.spans.get(*span_ref).ok_or_else(|| {
8234                ExcelError::new(ExcelErrorKind::NImpl)
8235                    .with_message("FormulaPlane active span ref is stale")
8236            })?;
8237            let result_region = Region::from_domain(span.result_region.domain());
8238            producer_results.insert_producer(FormulaProducerId::Span(span.id), result_region);
8239            let Some(read_summary_id) = span.read_summary_id else {
8240                return Err(ExcelError::new(ExcelErrorKind::NImpl)
8241                    .with_message("FormulaPlane active span is missing read summary"));
8242            };
8243            let Some(read_summary) = authority.plane.span_read_summaries.get(read_summary_id)
8244            else {
8245                return Err(ExcelError::new(ExcelErrorKind::NImpl)
8246                    .with_message("FormulaPlane active span has stale read summary"));
8247            };
8248            if read_summary.result_region != result_region {
8249                return Err(ExcelError::new(ExcelErrorKind::NImpl)
8250                    .with_message("FormulaPlane active span read summary is stale"));
8251            }
8252            for dependency in &read_summary.dependencies {
8253                consumer_reads.insert_read(
8254                    FormulaProducerId::Span(span.id),
8255                    dependency.read_region,
8256                    read_summary.result_region,
8257                    dependency.projection,
8258                );
8259            }
8260            if matches!(span_seed_mode, SpanSeedMode::WholeAll) {
8261                work.push(FormulaProducerWork {
8262                    producer: FormulaProducerId::Span(span.id),
8263                    dirty: ProducerDirtyDomain::Whole,
8264                });
8265            }
8266        }
8267
8268        let legacy_vertices = self.graph.formula_vertices();
8269        let mut scheduled_legacy_vertices = Vec::new();
8270        for vertex in &legacy_vertices {
8271            let Some(cell) = self.graph.get_cell_ref_for_vertex(*vertex) else {
8272                continue;
8273            };
8274            let result_region = Region::point(cell.sheet_id, cell.coord.row(), cell.coord.col());
8275            producer_results.insert_producer(FormulaProducerId::Legacy(*vertex), result_region);
8276            if dirty_legacy.contains(vertex) {
8277                scheduled_legacy_vertices.push(*vertex);
8278                work.push(FormulaProducerWork {
8279                    producer: FormulaProducerId::Legacy(*vertex),
8280                    dirty: ProducerDirtyDomain::Whole,
8281                });
8282            }
8283        }
8284
8285        for vertex in &legacy_vertices {
8286            let Some(cell) = self.graph.get_cell_ref_for_vertex(*vertex) else {
8287                continue;
8288            };
8289            let result_region = Region::point(cell.sheet_id, cell.coord.row(), cell.coord.col());
8290            let mut seen = rustc_hash::FxHashSet::default();
8291            for dep in self.graph.get_dependencies(*vertex) {
8292                let Some(dep_cell) = self.graph.get_cell_ref_for_vertex(dep) else {
8293                    continue;
8294                };
8295                let read_region = Region::point(
8296                    dep_cell.sheet_id,
8297                    dep_cell.coord.row(),
8298                    dep_cell.coord.col(),
8299                );
8300                if seen.insert(read_region) {
8301                    consumer_reads.insert_read(
8302                        FormulaProducerId::Legacy(*vertex),
8303                        read_region,
8304                        result_region,
8305                        DirtyProjectionRule::WholeResult,
8306                    );
8307                }
8308            }
8309            if let Some(ranges) = self.graph.get_range_dependencies(*vertex) {
8310                for range in ranges {
8311                    let Some(read_region) = self.shared_range_to_region_pattern(range)? else {
8312                        continue;
8313                    };
8314                    if seen.insert(read_region) {
8315                        consumer_reads.insert_read(
8316                            FormulaProducerId::Legacy(*vertex),
8317                            read_region,
8318                            result_region,
8319                            DirtyProjectionRule::WholeResult,
8320                        );
8321                    }
8322                }
8323            }
8324        }
8325
8326        // When span seed mode is DirtyClosure, derive bounded span work from
8327        // captured changed regions via the consumer-read index. This avoids
8328        // recomputing every active span on edits that only touch a small
8329        // number of cells.
8330        if matches!(span_seed_mode, SpanSeedMode::DirtyClosure)
8331            && !pending_changed_regions.is_empty()
8332        {
8333            use crate::formula_plane::producer::compute_dirty_closure;
8334            let producer_results_ref = &producer_results;
8335            let closure = compute_dirty_closure(
8336                &consumer_reads,
8337                pending_changed_regions.iter().copied(),
8338                |producer| producer_results_ref.producer_result_region(producer),
8339            );
8340            for fallback_work in closure.work {
8341                work.push(fallback_work);
8342            }
8343            // Any unsupported/conservative fallbacks for spans imply we may have
8344            // missed work; in that case demote to whole-span for affected spans.
8345            if !closure.fallbacks.is_empty() {
8346                let mut already_whole: rustc_hash::FxHashSet<_> = work
8347                    .iter()
8348                    .filter_map(|w| match (w.producer, &w.dirty) {
8349                        (FormulaProducerId::Span(id), ProducerDirtyDomain::Whole) => Some(id),
8350                        _ => None,
8351                    })
8352                    .collect();
8353                for fb in &closure.fallbacks {
8354                    if let FormulaProducerId::Span(id) = fb.consumer
8355                        && already_whole.insert(id)
8356                    {
8357                        work.push(FormulaProducerWork {
8358                            producer: FormulaProducerId::Span(id),
8359                            dirty: ProducerDirtyDomain::Whole,
8360                        });
8361                    }
8362                }
8363            }
8364        }
8365
8366        let schedule = build_mixed_schedule(work, &producer_results, &consumer_reads);
8367        Ok((
8368            schedule,
8369            span_refs_by_id,
8370            authority.plane.epoch().0,
8371            scheduled_legacy_vertices,
8372        ))
8373    }
8374}
8375
8376/// Strategy for seeding span producer work in the FP mixed runtime.
8377/// `WholeAll` schedules every active span as `Whole`; `DirtyClosure`
8378/// computes bounded work from captured changed regions only.
8379#[derive(Clone, Copy, Debug)]
8380enum SpanSeedMode {
8381    WholeAll,
8382    DirtyClosure,
8383}
8384
8385impl<R> Engine<R>
8386where
8387    R: EvaluationContext,
8388{
8389    fn shared_range_to_region_pattern(
8390        &self,
8391        range: &crate::reference::SharedRangeRef<'static>,
8392    ) -> Result<Option<Region>, ExcelError> {
8393        use crate::reference::SharedSheetLocator;
8394        let sheet_id = match range.sheet {
8395            SharedSheetLocator::Id(id) => id,
8396            SharedSheetLocator::Current => self.graph.default_sheet_id(),
8397            SharedSheetLocator::Name(_) => return Ok(None),
8398        };
8399        match (
8400            range.start_row,
8401            range.end_row,
8402            range.start_col,
8403            range.end_col,
8404        ) {
8405            (Some(sr), Some(er), Some(sc), Some(ec)) => Ok(Some(Region::rect(
8406                sheet_id, sr.index, er.index, sc.index, ec.index,
8407            ))),
8408            (None, None, Some(sc), Some(ec)) if sc.index == ec.index => {
8409                Ok(Some(Region::whole_col(sheet_id, sc.index)))
8410            }
8411            (Some(sr), Some(er), None, None) if sr.index == er.index => {
8412                Ok(Some(Region::whole_row(sheet_id, sr.index)))
8413            }
8414            _ => Ok(None),
8415        }
8416    }
8417
8418    /// Evaluate all dirty/volatile vertices
8419    pub fn evaluate_all(&mut self) -> Result<EvalResult, ExcelError> {
8420        self.lookup_index_cache.reset_counters();
8421        let _source_cache = self.source_cache_session();
8422        self.validate_deterministic_mode()?;
8423        if self.config.defer_graph_building {
8424            // Build graph for all staged formulas before evaluating
8425            self.build_graph_all()?;
8426        }
8427        self.evaluate_all_coordinator()
8428    }
8429
8430    /// Central FormulaPlane-aware coordinator for `evaluate_all`. In
8431    /// `AuthoritativeExperimental` mode every call enters the FormulaPlane
8432    /// coordinator; the coordinator itself composes with private legacy
8433    /// primitives for legacy-only work.
8434    fn evaluate_all_coordinator(&mut self) -> Result<EvalResult, ExcelError> {
8435        if self.config.formula_plane_mode == FormulaPlaneMode::AuthoritativeExperimental {
8436            return self.evaluate_authoritative_formula_plane_all();
8437        }
8438        self.evaluate_all_legacy_impl()
8439    }
8440
8441    fn legacy_pass_apply_cycles(&mut self, schedule: &crate::engine::scheduler::Schedule) -> usize {
8442        let circ_error = LiteralValue::Error(
8443            ExcelError::new(ExcelErrorKind::Circ)
8444                .with_message("Circular dependency detected".to_string()),
8445        );
8446        for cycle in &schedule.cycles {
8447            for &vertex_id in cycle {
8448                self.graph
8449                    .update_vertex_value(vertex_id, circ_error.clone());
8450                self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
8451            }
8452        }
8453        schedule.cycles.len()
8454    }
8455
8456    fn legacy_pass_run_layers(
8457        &mut self,
8458        schedule: &crate::engine::scheduler::Schedule,
8459    ) -> Result<usize, ExcelError> {
8460        let mut computed_vertices = 0;
8461        for layer in &schedule.layers {
8462            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
8463                computed_vertices += self.evaluate_layer_parallel(layer)?;
8464            } else {
8465                computed_vertices += self.evaluate_layer_sequential(layer)?;
8466            }
8467        }
8468        Ok(computed_vertices)
8469    }
8470
8471    /// Legacy `evaluate_all` body, reachable from the FormulaPlane coordinator
8472    /// when no active spans exist or FormulaPlane authority is not in
8473    /// `AuthoritativeExperimental` mode. This is now an internal primitive; it
8474    /// must not be invoked directly from public APIs.
8475    fn evaluate_all_legacy_impl(&mut self) -> Result<EvalResult, ExcelError> {
8476        self.reset_virtual_dep_telemetry_if_disabled();
8477        #[cfg(feature = "tracing")]
8478        let _span_eval = tracing::info_span!("evaluate_all").entered();
8479        let start = crate::instant::FzInstant::now();
8480        let mut computed_vertices = 0;
8481        let mut cycle_errors = 0;
8482        let mut replan_iterations = 0;
8483        const MAX_REPLAN: usize = 5;
8484        let mut telemetry = self
8485            .config
8486            .enable_virtual_dep_telemetry
8487            .then(|| self.start_virtual_dep_telemetry());
8488
8489        loop {
8490            let to_evaluate = self.graph.get_evaluation_vertices();
8491            if to_evaluate.is_empty() {
8492                if let Some(t) = telemetry.as_mut()
8493                    && t.bailout_reason.is_none()
8494                {
8495                    t.bailout_reason = Some("no_work");
8496                }
8497                break;
8498            }
8499
8500            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
8501            if let Some(t) = telemetry.as_mut() {
8502                Self::accumulate_schedule_meta(t, &meta);
8503            }
8504
8505            cycle_errors += self.legacy_pass_apply_cycles(&schedule);
8506            computed_vertices += self.legacy_pass_run_layers(&schedule)?;
8507
8508            // Check if dynamic dependencies changed
8509            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
8510            if let Some(t) = telemetry.as_mut() {
8511                t.changed_vdeps_total += changed_vertices.len();
8512            }
8513
8514            self.graph.clear_dirty_flags(&to_evaluate);
8515            for v in &changed_vertices {
8516                self.graph.set_dirty(*v, true);
8517            }
8518
8519            if changed_vertices.is_empty() {
8520                if let Some(t) = telemetry.as_mut() {
8521                    t.bailout_reason = Some("converged");
8522                }
8523                break;
8524            }
8525            if replan_iterations >= MAX_REPLAN {
8526                if let Some(t) = telemetry.as_mut() {
8527                    t.bailout_reason = Some("max_replan");
8528                }
8529                break;
8530            }
8531
8532            replan_iterations += 1;
8533        }
8534
8535        if let Some(mut t) = telemetry {
8536            t.replan_iterations = replan_iterations;
8537            self.last_virtual_dep_telemetry = t;
8538        }
8539
8540        // Re-dirty volatile vertices for the next evaluation cycle
8541        self.graph.redirty_volatiles();
8542
8543        // Advance recalc epoch after a full evaluation pass finishes
8544        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
8545
8546        Ok(EvalResult {
8547            computed_vertices,
8548            cycle_errors,
8549            elapsed: start.elapsed(),
8550        })
8551    }
8552
8553    pub fn evaluate_all_with_delta(&mut self) -> Result<(EvalResult, EvalDelta), ExcelError> {
8554        let mut collector = DeltaCollector::new(DeltaMode::Cells);
8555        let result = self.evaluate_all_with_delta_collector(&mut collector)?;
8556        Ok((result, collector.finish()))
8557    }
8558
8559    fn evaluate_all_with_delta_collector(
8560        &mut self,
8561        delta: &mut DeltaCollector,
8562    ) -> Result<EvalResult, ExcelError> {
8563        let _source_cache = self.source_cache_session();
8564        if self.config.defer_graph_building {
8565            self.build_graph_all()?;
8566        }
8567        if self.graph.formula_authority().active_span_count() > 0 {
8568            let _ = delta;
8569            return self.evaluate_authoritative_formula_plane_all();
8570        }
8571        self.reset_virtual_dep_telemetry_if_disabled();
8572        #[cfg(feature = "tracing")]
8573        let _span_eval = tracing::info_span!("evaluate_all_with_delta").entered();
8574        let start = crate::instant::FzInstant::now();
8575        let mut computed_vertices = 0;
8576        let mut cycle_errors = 0;
8577
8578        let mut replan_iterations = 0;
8579        const MAX_REPLAN: usize = 5;
8580        let mut telemetry = self
8581            .config
8582            .enable_virtual_dep_telemetry
8583            .then(|| self.start_virtual_dep_telemetry());
8584
8585        loop {
8586            let to_evaluate = self.graph.get_evaluation_vertices();
8587            if to_evaluate.is_empty() {
8588                if let Some(t) = telemetry.as_mut()
8589                    && t.bailout_reason.is_none()
8590                {
8591                    t.bailout_reason = Some("no_work");
8592                }
8593                break;
8594            }
8595
8596            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
8597            if let Some(t) = telemetry.as_mut() {
8598                Self::accumulate_schedule_meta(t, &meta);
8599            }
8600
8601            let circ_error = LiteralValue::Error(
8602                ExcelError::new(ExcelErrorKind::Circ)
8603                    .with_message("Circular dependency detected".to_string()),
8604            );
8605            for cycle in &schedule.cycles {
8606                cycle_errors += 1;
8607                for &vertex_id in cycle {
8608                    if delta.mode != DeltaMode::Off
8609                        && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
8610                    {
8611                        let sheet_name = self.graph.sheet_name(cell.sheet_id);
8612                        let old = self
8613                            .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
8614                            .unwrap_or(LiteralValue::Empty);
8615                        if old != circ_error {
8616                            delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
8617                        }
8618                    }
8619                    self.graph
8620                        .update_vertex_value(vertex_id, circ_error.clone());
8621                    self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
8622                }
8623            }
8624
8625            for layer in &schedule.layers {
8626                if self.thread_pool.is_some() && layer.vertices.len() > 1 {
8627                    computed_vertices += self.evaluate_layer_parallel_with_delta(layer, delta)?;
8628                } else {
8629                    computed_vertices += self.evaluate_layer_sequential_with_delta(layer, delta)?;
8630                }
8631            }
8632
8633            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
8634            if let Some(t) = telemetry.as_mut() {
8635                t.changed_vdeps_total += changed_vertices.len();
8636            }
8637            self.graph.clear_dirty_flags(&to_evaluate);
8638            for v in &changed_vertices {
8639                self.graph.set_dirty(*v, true);
8640            }
8641
8642            if changed_vertices.is_empty() {
8643                if let Some(t) = telemetry.as_mut() {
8644                    t.bailout_reason = Some("converged");
8645                }
8646                break;
8647            }
8648            if replan_iterations >= MAX_REPLAN {
8649                if let Some(t) = telemetry.as_mut() {
8650                    t.bailout_reason = Some("max_replan");
8651                }
8652                break;
8653            }
8654            replan_iterations += 1;
8655        }
8656
8657        if let Some(mut t) = telemetry {
8658            t.replan_iterations = replan_iterations;
8659            self.last_virtual_dep_telemetry = t;
8660        }
8661
8662        self.graph.redirty_volatiles();
8663        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
8664
8665        Ok(EvalResult {
8666            computed_vertices,
8667            cycle_errors,
8668            elapsed: start.elapsed(),
8669        })
8670    }
8671
8672    /// Convenience: demand-driven evaluation of a single cell by sheet name and row/col.
8673    ///
8674    /// This will evaluate only the minimal set of dirty / volatile precedents required
8675    /// to bring the target cell up-to-date (as if a user asked for that single value),
8676    /// rather than scheduling a full workbook recalc. If the cell is already clean and
8677    /// non-volatile, no vertices will be recomputed.
8678    ///
8679    /// Returns the (possibly newly computed) value stored for the cell afterwards.
8680    /// Empty cells return None. Errors are surfaced via the Result type.
8681    pub fn evaluate_cell(
8682        &mut self,
8683        sheet: &str,
8684        row: u32,
8685        col: u32,
8686    ) -> Result<Option<LiteralValue>, ExcelError> {
8687        if row == 0 || col == 0 {
8688            return Err(ExcelError::new(ExcelErrorKind::Ref)
8689                .with_message("Row and column must be >= 1".to_string()));
8690        }
8691
8692        // ``defer_graph_building`` mode stages formulas during bulk load
8693        // and lazily promotes them into the dependency graph at evaluate
8694        // time. Per-cell evaluation must drain *all* staged sheets, not
8695        // just the requested target — a cell's formula can reference
8696        // any sheet in the workbook, and a cross-sheet ref to a still-
8697        // staged source would silently evaluate to ``None`` if that
8698        // source sheet hadn't been promoted yet.
8699        if self.config.defer_graph_building {
8700            self.build_graph_all()?;
8701        }
8702
8703        let result = self.evaluate_cells(&[(sheet, row, col)])?;
8704
8705        match result.len() {
8706            0 => Ok(None),
8707            1 => {
8708                let v = result.into_iter().next().unwrap();
8709                Ok(v)
8710            }
8711            _ => unreachable!("evaluate_cells returned unexpected length"),
8712        }
8713    }
8714
8715    /// Convenience: demand-driven evaluation of multiple cells; accepts a slice of
8716    /// (sheet, row, col) triples. The union of required dirty / volatile precedents
8717    /// is computed once and evaluated, which is typically faster than calling
8718    /// `evaluate_cell` repeatedly for a related set of targets.
8719    ///
8720    /// Returns the resulting values for each requested target in the same order.
8721    pub fn evaluate_cells(
8722        &mut self,
8723        targets: &[(&str, u32, u32)],
8724    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
8725        self.validate_deterministic_mode()?;
8726        if targets.is_empty() {
8727            return Ok(Vec::new());
8728        }
8729        // See ``evaluate_cell`` for why we drain *all* staged sheets in
8730        // ``defer_graph_building`` mode: cross-sheet refs to still-staged
8731        // sources would otherwise evaluate to ``None``.
8732        if self.config.defer_graph_building {
8733            self.build_graph_all()?;
8734        }
8735        if self.graph.formula_authority().active_span_count() > 0 {
8736            let _ = self.evaluate_authoritative_formula_plane_all()?;
8737        } else {
8738            self.evaluate_until(targets)?;
8739        }
8740        Ok(targets
8741            .iter()
8742            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
8743            .collect())
8744    }
8745
8746    pub fn evaluate_cells_cancellable(
8747        &mut self,
8748        targets: &[(&str, u32, u32)],
8749        cancel_flag: Arc<AtomicBool>,
8750    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
8751        self.active_cancel_flag = Some(cancel_flag.clone());
8752        let res = self.evaluate_cells_cancellable_impl(targets, &cancel_flag);
8753        self.active_cancel_flag = None;
8754        res
8755    }
8756
8757    fn evaluate_cells_cancellable_impl(
8758        &mut self,
8759        targets: &[(&str, u32, u32)],
8760        cancel_flag: &AtomicBool,
8761    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
8762        self.validate_deterministic_mode()?;
8763        if targets.is_empty() {
8764            return Ok(Vec::new());
8765        }
8766        // See ``evaluate_cell`` for why we drain *all* staged sheets in
8767        // ``defer_graph_building`` mode: cross-sheet refs to still-staged
8768        // sources would otherwise evaluate to ``None``.
8769        if self.config.defer_graph_building {
8770            self.build_graph_all()?;
8771        }
8772        if self.graph.formula_authority().active_span_count() > 0 {
8773            if cancel_flag.load(Ordering::Relaxed) {
8774                return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
8775                    "Evaluation cancelled before FormulaPlane scheduling".to_string(),
8776                ));
8777            }
8778            let _ = self.evaluate_authoritative_formula_plane_all()?;
8779            return Ok(targets
8780                .iter()
8781                .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
8782                .collect());
8783        }
8784
8785        // evaluate_until_cancellable takes &[&str] in A1 notation, but we have (&str, u32, u32)
8786        // Let's implement evaluate_until_coords_cancellable or similar, or just convert
8787        let a1_targets: Vec<String> = targets
8788            .iter()
8789            .map(|(s, r, c)| {
8790                format!("{}!{}", s, col_letters_from_1based(*c).unwrap()) + &r.to_string()
8791            })
8792            .collect();
8793        let a1_refs: Vec<&str> = a1_targets.iter().map(|s| s.as_str()).collect();
8794
8795        self.evaluate_until_cancellable_impl(&a1_refs, cancel_flag)?;
8796
8797        Ok(targets
8798            .iter()
8799            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
8800            .collect())
8801    }
8802
8803    pub fn evaluate_cells_with_delta(
8804        &mut self,
8805        targets: &[(&str, u32, u32)],
8806    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
8807        self.validate_deterministic_mode()?;
8808        if targets.is_empty() {
8809            return Ok((Vec::new(), EvalDelta::default()));
8810        }
8811        if self.config.defer_graph_building {
8812            let mut sheets: rustc_hash::FxHashSet<&str> = rustc_hash::FxHashSet::default();
8813            for (s, _, _) in targets.iter() {
8814                sheets.insert(*s);
8815            }
8816            self.build_graph_for_sheets(sheets.iter().cloned())?;
8817        }
8818        if self.graph.formula_authority().active_span_count() > 0 {
8819            let _ = self.evaluate_authoritative_formula_plane_all()?;
8820            let values = targets
8821                .iter()
8822                .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
8823                .collect();
8824            return Ok((values, EvalDelta::default()));
8825        }
8826        let mut collector = DeltaCollector::new(DeltaMode::Cells);
8827        self.evaluate_until_with_delta_collector(targets, &mut collector)?;
8828        let values = targets
8829            .iter()
8830            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
8831            .collect();
8832        Ok((values, collector.finish()))
8833    }
8834
8835    /// Get the evaluation plan for target cells without actually evaluating them
8836    pub fn get_eval_plan(&self, targets: &[(&str, u32, u32)]) -> Result<EvalPlan, ExcelError> {
8837        if targets.is_empty() {
8838            return Ok(EvalPlan {
8839                total_vertices_to_evaluate: 0,
8840                layers: Vec::new(),
8841                cycles_detected: 0,
8842                dirty_count: 0,
8843                volatile_count: 0,
8844                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
8845                estimated_parallel_layers: 0,
8846                target_cells: Vec::new(),
8847            });
8848        }
8849        if self.config.defer_graph_building && self.has_staged_formulas() {
8850            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
8851                "Evaluation plan requested with deferred graph; build first or call evaluate_*",
8852            ));
8853        }
8854
8855        // Convert targets to A1 notation for consistency
8856        let addresses: Vec<String> = targets
8857            .iter()
8858            .map(|(s, r, c)| format!("{}!{}{}", s, Self::col_to_letters(*c), r))
8859            .collect();
8860
8861        // Parse target cell addresses
8862        let mut target_addrs = Vec::new();
8863        for (sheet, row, col) in targets {
8864            if let Some(sheet_id) = self.graph.sheet_id(sheet) {
8865                let coord = Coord::from_excel(*row, *col, true, true);
8866                target_addrs.push(CellRef::new(sheet_id, coord));
8867            }
8868        }
8869
8870        // Find vertex IDs for targets
8871        let mut target_vertex_ids = Vec::new();
8872        for addr in &target_addrs {
8873            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
8874                target_vertex_ids.push(*vertex_id);
8875            }
8876        }
8877
8878        if target_vertex_ids.is_empty() {
8879            return Ok(EvalPlan {
8880                total_vertices_to_evaluate: 0,
8881                layers: Vec::new(),
8882                cycles_detected: 0,
8883                dirty_count: 0,
8884                volatile_count: 0,
8885                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
8886                estimated_parallel_layers: 0,
8887                target_cells: addresses,
8888            });
8889        }
8890
8891        // Build demand subgraph with virtual edges (same as evaluate_until)
8892        let (precedents_to_eval, vdeps) = self.build_demand_subgraph(&target_vertex_ids);
8893
8894        if precedents_to_eval.is_empty() {
8895            return Ok(EvalPlan {
8896                total_vertices_to_evaluate: 0,
8897                layers: Vec::new(),
8898                cycles_detected: 0,
8899                dirty_count: 0,
8900                volatile_count: 0,
8901                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
8902                estimated_parallel_layers: 0,
8903                target_cells: addresses,
8904            });
8905        }
8906
8907        // Count dirty and volatile vertices
8908        let mut dirty_count = 0;
8909        let mut volatile_count = 0;
8910        for &vertex_id in &precedents_to_eval {
8911            if self.graph.is_dirty(vertex_id) {
8912                dirty_count += 1;
8913            }
8914            if self.graph.is_volatile(vertex_id) {
8915                volatile_count += 1;
8916            }
8917        }
8918
8919        // Create schedule for the minimal subgraph honoring virtual edges
8920        let scheduler = Scheduler::new(&self.graph);
8921        let schedule = scheduler.create_schedule_with_virtual(&precedents_to_eval, &vdeps)?;
8922
8923        // Build layer information
8924        let mut layers = Vec::new();
8925        let mut estimated_parallel_layers = 0;
8926        let parallel_enabled = self.config.enable_parallel && self.thread_pool.is_some();
8927
8928        for layer in &schedule.layers {
8929            let parallel_eligible = parallel_enabled && layer.vertices.len() > 1;
8930            if parallel_eligible {
8931                estimated_parallel_layers += 1;
8932            }
8933
8934            // Get sample cell addresses (up to 5)
8935            let sample_cells: Vec<String> = layer
8936                .vertices
8937                .iter()
8938                .take(5)
8939                .filter_map(|&vertex_id| {
8940                    self.graph
8941                        .get_cell_ref_for_vertex(vertex_id)
8942                        .map(|cell_ref| {
8943                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
8944                            format!(
8945                                "{}!{}{}",
8946                                sheet_name,
8947                                Self::col_to_letters(cell_ref.coord.col()),
8948                                cell_ref.coord.row() + 1
8949                            )
8950                        })
8951                })
8952                .collect();
8953
8954            layers.push(LayerInfo {
8955                vertex_count: layer.vertices.len(),
8956                parallel_eligible,
8957                sample_cells,
8958            });
8959        }
8960
8961        Ok(EvalPlan {
8962            total_vertices_to_evaluate: precedents_to_eval.len(),
8963            layers,
8964            cycles_detected: schedule.cycles.len(),
8965            dirty_count,
8966            volatile_count,
8967            parallel_enabled,
8968            estimated_parallel_layers,
8969            target_cells: addresses,
8970        })
8971    }
8972    /// Helper to create a schedule, integrating virtual dependencies automatically.
8973    fn create_evaluation_schedule(
8974        &mut self,
8975        to_evaluate: &[VertexId],
8976    ) -> Result<ScheduleBuildOutput, ExcelError> {
8977        if self.can_use_static_schedule_cache(to_evaluate) {
8978            if let Some(cached) = self.cached_static_schedule.as_ref()
8979                && cached.topology_epoch == self.topology_epoch
8980                && cached.candidate_vertices.as_slice() == to_evaluate
8981            {
8982                let meta = ScheduleBuildMeta {
8983                    candidate_vertices: to_evaluate.len(),
8984                    vdeps_vertices: 0,
8985                    vdeps_edges: 0,
8986                    builder_elapsed_ms: 0,
8987                    used_virtual_schedule: false,
8988                    schedule_cache_hit: true,
8989                    schedule_cache_eligible: true,
8990                };
8991                return Ok((cached.schedule.clone(), FxHashMap::default(), meta));
8992            }
8993
8994            let (schedule, vdeps, mut meta) =
8995                self.create_evaluation_schedule_uncached(to_evaluate)?;
8996            meta.schedule_cache_hit = false;
8997            meta.schedule_cache_eligible = true;
8998            if vdeps.is_empty() {
8999                self.cached_static_schedule = Some(CachedScheduleEntry {
9000                    topology_epoch: self.topology_epoch,
9001                    candidate_vertices: to_evaluate.to_vec(),
9002                    schedule: schedule.clone(),
9003                });
9004            }
9005            return Ok((schedule, vdeps, meta));
9006        }
9007
9008        let (schedule, vdeps, mut meta) = self.create_evaluation_schedule_uncached(to_evaluate)?;
9009        meta.schedule_cache_hit = false;
9010        meta.schedule_cache_eligible = false;
9011        Ok((schedule, vdeps, meta))
9012    }
9013
9014    fn create_evaluation_schedule_uncached(
9015        &self,
9016        to_evaluate: &[VertexId],
9017    ) -> Result<ScheduleBuildOutput, ExcelError> {
9018        let builder = VirtualDepBuilder::new(self);
9019        let (vdeps, augmented, builder_elapsed_ms, vdeps_edges) =
9020            if self.config.enable_virtual_dep_telemetry {
9021                let build_started = crate::instant::FzInstant::now();
9022                let (vdeps, augmented) = builder.build(to_evaluate);
9023                let builder_elapsed_ms = build_started.elapsed().as_millis();
9024                let vdeps_edges = vdeps.values().map(|deps| deps.len()).sum::<usize>();
9025                (vdeps, augmented, builder_elapsed_ms, vdeps_edges)
9026            } else {
9027                let (vdeps, augmented) = builder.build(to_evaluate);
9028                (vdeps, augmented, 0, 0)
9029            };
9030
9031        let mut final_evaluate = to_evaluate.to_vec();
9032        if !augmented.is_empty() {
9033            final_evaluate.extend(augmented);
9034            final_evaluate.sort_unstable();
9035            final_evaluate.dedup();
9036        }
9037
9038        let use_virtual = !vdeps.is_empty();
9039
9040        let scheduler = Scheduler::new(&self.graph);
9041        let schedule = if use_virtual {
9042            scheduler.create_schedule_with_virtual(&final_evaluate, &vdeps)?
9043        } else {
9044            scheduler.create_schedule(&final_evaluate)?
9045        };
9046
9047        let meta = ScheduleBuildMeta {
9048            candidate_vertices: to_evaluate.len(),
9049            vdeps_vertices: vdeps.len(),
9050            vdeps_edges,
9051            builder_elapsed_ms,
9052            used_virtual_schedule: use_virtual,
9053            schedule_cache_hit: false,
9054            schedule_cache_eligible: false,
9055        };
9056
9057        Ok((schedule, vdeps, meta))
9058    }
9059
9060    fn can_use_static_schedule_cache(&self, to_evaluate: &[VertexId]) -> bool {
9061        !to_evaluate.is_empty()
9062            && to_evaluate.iter().copied().all(|v| {
9063                !self.graph.is_dynamic(v) && self.graph.get_range_dependencies(v).is_none()
9064            })
9065    }
9066
9067    fn start_virtual_dep_telemetry(&self) -> VirtualDepTelemetry {
9068        VirtualDepTelemetry {
9069            fallback_mode_activations: self.virtual_dep_fallback_activations,
9070            ..VirtualDepTelemetry::default()
9071        }
9072    }
9073
9074    fn accumulate_schedule_meta(telemetry: &mut VirtualDepTelemetry, meta: &ScheduleBuildMeta) {
9075        telemetry.candidate_vertices_total += meta.candidate_vertices;
9076        telemetry.vdeps_vertices_total += meta.vdeps_vertices;
9077        telemetry.vdeps_edges_total += meta.vdeps_edges;
9078        telemetry.builder_elapsed_ms_total += meta.builder_elapsed_ms;
9079        if meta.schedule_cache_eligible {
9080            if meta.schedule_cache_hit {
9081                telemetry.schedule_cache_hits += 1;
9082                telemetry.reused_schedule_vertices_total += meta.candidate_vertices;
9083            } else {
9084                telemetry.schedule_cache_misses += 1;
9085            }
9086        }
9087        if meta.used_virtual_schedule {
9088            telemetry.schedule_virtual_passes += 1;
9089        } else {
9090            telemetry.schedule_static_passes += 1;
9091        }
9092    }
9093
9094    fn changed_virtual_dep_vertices(
9095        &self,
9096        to_evaluate: &[VertexId],
9097        old_vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
9098    ) -> Vec<VertexId> {
9099        if !to_evaluate
9100            .iter()
9101            .copied()
9102            .any(|v| self.graph.is_dynamic(v))
9103        {
9104            return Vec::new();
9105        }
9106
9107        let builder = VirtualDepBuilder::new(self);
9108        let (new_vdeps, _) = builder.build(to_evaluate);
9109
9110        let mut candidates = FxHashSet::default();
9111        candidates.extend(old_vdeps.keys().copied());
9112        candidates.extend(new_vdeps.keys().copied());
9113
9114        let mut changed = Vec::new();
9115        for v in candidates {
9116            if old_vdeps.get(&v) != new_vdeps.get(&v) {
9117                changed.push(v);
9118            }
9119        }
9120        changed
9121    }
9122
9123    /// Build a demand-driven subgraph for the given targets, including ephemeral edges for
9124    /// compressed ranges, and returning the set of dirty/volatile precedents and virtual deps.
9125    fn build_demand_subgraph(
9126        &self,
9127        target_vertices: &[VertexId],
9128    ) -> (
9129        Vec<VertexId>,
9130        rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
9131    ) {
9132        #[cfg(feature = "tracing")]
9133        let _span =
9134            tracing::info_span!("demand_subgraph", targets = target_vertices.len()).entered();
9135        use rustc_hash::{FxHashMap, FxHashSet};
9136
9137        let mut to_evaluate: FxHashSet<VertexId> = FxHashSet::default();
9138        let mut visited: FxHashSet<VertexId> = FxHashSet::default();
9139        let mut stack: Vec<VertexId> = Vec::new();
9140        let mut vdeps: FxHashMap<VertexId, Vec<VertexId>> = FxHashMap::default(); // incoming deps per vertex
9141
9142        for &t in target_vertices {
9143            stack.push(t);
9144        }
9145
9146        while let Some(v) = stack.pop() {
9147            if !visited.insert(v) {
9148                continue;
9149            }
9150            if !self.graph.vertex_exists(v) {
9151                continue;
9152            }
9153            // Schedule dirty/volatile formulas. Also schedule pass-through
9154            // Named*/Range vertices so the scheduler honours the
9155            // topological position of any formula cells that sit underneath
9156            // them — without these in `vertex_set` the scheduler skips the
9157            // edges that route a target through a named-range vertex into
9158            // its underlying cells, and the underlying cells then end up
9159            // in the same (or an earlier) layer as the target.
9160            match self.graph.get_vertex_kind(v) {
9161                VertexKind::FormulaScalar | VertexKind::FormulaArray => {
9162                    if self.graph.is_dirty(v) || self.graph.is_volatile(v) {
9163                        to_evaluate.insert(v);
9164                    }
9165                }
9166                VertexKind::NamedScalar
9167                | VertexKind::NamedArray
9168                | VertexKind::Range
9169                | VertexKind::InfiniteRange => {
9170                    to_evaluate.insert(v);
9171                }
9172                _ => {}
9173            }
9174
9175            // Explicit dependencies (graph edges). We push *every* dep onto
9176            // the stack — not just formulas — because intermediate vertices
9177            // (NamedScalar, NamedArray, Range) are pass-through nodes whose
9178            // own dependencies point at the actual formula cells. Filtering
9179            // by kind here previously caused DN-range refs to be dropped
9180            // from the demand subgraph, so a target like
9181            // ``=SUM(named_range_pointing_at_dirty_cells)`` would evaluate
9182            // using stale values for those cells. The kind check at the top
9183            // of the loop still gates which vertices end up in
9184            // ``to_evaluate``; only Formula vertices are scheduled.
9185            if let Some(dependencies) = self.graph.dependencies_slice(v) {
9186                for &dep in dependencies {
9187                    if self.graph.vertex_exists(dep) && !visited.contains(&dep) {
9188                        stack.push(dep);
9189                    }
9190                }
9191            } else {
9192                for dep in self.graph.get_dependencies(v) {
9193                    if self.graph.vertex_exists(dep) && !visited.contains(&dep) {
9194                        stack.push(dep);
9195                    }
9196                }
9197            } // Virtual dependencies (compressed ranges + dynamic like INDIRECT)
9198            let builder = VirtualDepBuilder::new(self);
9199            let (vdeps_map, _) = builder.build(&[v]);
9200            if let Some(deps) = vdeps_map.get(&v) {
9201                for &u in deps {
9202                    vdeps.entry(v).or_default().push(u);
9203                    if !visited.contains(&u) {
9204                        stack.push(u);
9205                    }
9206                }
9207            }
9208        }
9209
9210        let mut result: Vec<VertexId> = to_evaluate.into_iter().collect();
9211        result.sort_unstable();
9212        // Dedup virtual deps
9213        for deps in vdeps.values_mut() {
9214            deps.sort_unstable();
9215            deps.dedup();
9216        }
9217        (result, vdeps)
9218    }
9219
9220    /// Helper: convert 1-based column index to Excel-style letters (1 -> A, 27 -> AA)
9221    fn col_to_letters(col: u32) -> String {
9222        col_letters_from_1based(col).expect("column index must be >= 1")
9223    }
9224
9225    /// Evaluate all dirty/volatile vertices with cancellation support
9226    pub fn evaluate_all_cancellable(
9227        &mut self,
9228        cancel_flag: Arc<AtomicBool>,
9229    ) -> Result<EvalResult, ExcelError> {
9230        self.active_cancel_flag = Some(cancel_flag.clone());
9231        let res = self.evaluate_all_cancellable_impl(&cancel_flag);
9232        self.active_cancel_flag = None;
9233        res
9234    }
9235
9236    fn evaluate_all_cancellable_impl(
9237        &mut self,
9238        cancel_flag: &AtomicBool,
9239    ) -> Result<EvalResult, ExcelError> {
9240        let _source_cache = self.source_cache_session();
9241        self.validate_deterministic_mode()?;
9242        if self.config.defer_graph_building {
9243            self.build_graph_all()?;
9244        }
9245        if self.graph.formula_authority().active_span_count() > 0 {
9246            if cancel_flag.load(Ordering::Relaxed) {
9247                return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
9248                    "Evaluation cancelled before FormulaPlane scheduling".to_string(),
9249                ));
9250            }
9251            return self.evaluate_authoritative_formula_plane_all();
9252        }
9253        self.reset_virtual_dep_telemetry_if_disabled();
9254        let start = crate::instant::FzInstant::now();
9255        let mut computed_vertices = 0;
9256        let mut cycle_errors = 0;
9257
9258        let mut replan_iterations = 0;
9259        const MAX_REPLAN: usize = 5;
9260        let mut telemetry = self
9261            .config
9262            .enable_virtual_dep_telemetry
9263            .then(|| self.start_virtual_dep_telemetry());
9264
9265        loop {
9266            if cancel_flag.load(Ordering::Relaxed) {
9267                if let Some(mut t) = telemetry {
9268                    t.bailout_reason = Some("cancelled");
9269                    t.replan_iterations = replan_iterations;
9270                    self.last_virtual_dep_telemetry = t;
9271                }
9272                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
9273                    .with_message("Evaluation cancelled before scheduling".to_string()));
9274            }
9275
9276            let to_evaluate = self.graph.get_evaluation_vertices();
9277            if to_evaluate.is_empty() {
9278                if let Some(t) = telemetry.as_mut()
9279                    && t.bailout_reason.is_none()
9280                {
9281                    t.bailout_reason = Some("no_work");
9282                }
9283                break;
9284            }
9285
9286            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
9287            if let Some(t) = telemetry.as_mut() {
9288                Self::accumulate_schedule_meta(t, &meta);
9289            }
9290
9291            // Handle cycles first by marking them with #CIRC!
9292            for cycle in &schedule.cycles {
9293                // Check cancellation between cycles
9294                if cancel_flag.load(Ordering::Relaxed) {
9295                    if let Some(mut t) = telemetry {
9296                        t.bailout_reason = Some("cancelled");
9297                        t.replan_iterations = replan_iterations;
9298                        self.last_virtual_dep_telemetry = t;
9299                    }
9300                    return Err(ExcelError::new(ExcelErrorKind::Cancelled)
9301                        .with_message("Evaluation cancelled during cycle handling".to_string()));
9302                }
9303
9304                cycle_errors += 1;
9305                let circ_error = LiteralValue::Error(
9306                    ExcelError::new(ExcelErrorKind::Circ)
9307                        .with_message("Circular dependency detected".to_string()),
9308                );
9309                for &vertex_id in cycle {
9310                    self.graph
9311                        .update_vertex_value(vertex_id, circ_error.clone());
9312                    self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
9313                }
9314            }
9315
9316            // Evaluate acyclic layers sequentially with cancellation checks
9317            for layer in &schedule.layers {
9318                // Check cancellation between layers
9319                if cancel_flag.load(Ordering::Relaxed) {
9320                    if let Some(mut t) = telemetry {
9321                        t.bailout_reason = Some("cancelled");
9322                        t.replan_iterations = replan_iterations;
9323                        self.last_virtual_dep_telemetry = t;
9324                    }
9325                    return Err(ExcelError::new(ExcelErrorKind::Cancelled)
9326                        .with_message("Evaluation cancelled between layers".to_string()));
9327                }
9328
9329                // Evaluate vertices in this layer (parallel or sequential)
9330                if self.thread_pool.is_some() && layer.vertices.len() > 1 {
9331                    computed_vertices +=
9332                        self.evaluate_layer_parallel_cancellable(layer, cancel_flag)?;
9333                } else {
9334                    computed_vertices +=
9335                        self.evaluate_layer_sequential_cancellable(layer, cancel_flag)?;
9336                }
9337            }
9338
9339            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
9340            if let Some(t) = telemetry.as_mut() {
9341                t.changed_vdeps_total += changed_vertices.len();
9342            }
9343            self.graph.clear_dirty_flags(&to_evaluate);
9344            for v in &changed_vertices {
9345                self.graph.set_dirty(*v, true);
9346            }
9347
9348            if changed_vertices.is_empty() {
9349                if let Some(t) = telemetry.as_mut() {
9350                    t.bailout_reason = Some("converged");
9351                }
9352                break;
9353            }
9354            if replan_iterations >= MAX_REPLAN {
9355                if let Some(t) = telemetry.as_mut() {
9356                    t.bailout_reason = Some("max_replan");
9357                }
9358                break;
9359            }
9360            replan_iterations += 1;
9361        }
9362
9363        if let Some(mut t) = telemetry {
9364            t.replan_iterations = replan_iterations;
9365            self.last_virtual_dep_telemetry = t;
9366        }
9367
9368        // Re-dirty volatile vertices for the next evaluation cycle
9369        self.graph.redirty_volatiles();
9370        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
9371
9372        Ok(EvalResult {
9373            computed_vertices,
9374            cycle_errors,
9375            elapsed: start.elapsed(),
9376        })
9377    }
9378
9379    /// Evaluate only the necessary precedents for specific target cells with cancellation support
9380    pub fn evaluate_until_cancellable(
9381        &mut self,
9382        targets: &[&str],
9383        cancel_flag: Arc<AtomicBool>,
9384    ) -> Result<EvalResult, ExcelError> {
9385        self.active_cancel_flag = Some(cancel_flag.clone());
9386        let res = self.evaluate_until_cancellable_impl(targets, &cancel_flag);
9387        self.active_cancel_flag = None;
9388        res
9389    }
9390
9391    fn evaluate_until_cancellable_impl(
9392        &mut self,
9393        targets: &[&str],
9394        cancel_flag: &AtomicBool,
9395    ) -> Result<EvalResult, ExcelError> {
9396        let start = crate::instant::FzInstant::now();
9397        if self.graph.formula_authority().active_span_count() > 0 {
9398            if cancel_flag.load(Ordering::Relaxed) {
9399                return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
9400                    "Evaluation cancelled before FormulaPlane scheduling".to_string(),
9401                ));
9402            }
9403            return self.evaluate_authoritative_formula_plane_all();
9404        }
9405
9406        // Parse target cell addresses
9407        let mut target_addrs = Vec::new();
9408        for target in targets {
9409            let (sheet, row, col) = self.parse_a1_notation(target)?;
9410            let sheet_id = self.graph.sheet_id_mut(&sheet);
9411            let coord = Coord::from_excel(row, col, true, true);
9412            target_addrs.push(CellRef::new(sheet_id, coord));
9413        }
9414
9415        // Find vertex IDs for targets
9416        let mut target_vertex_ids = Vec::new();
9417        for addr in &target_addrs {
9418            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
9419                target_vertex_ids.push(*vertex_id);
9420            }
9421        }
9422
9423        if target_vertex_ids.is_empty() {
9424            return Ok(EvalResult {
9425                computed_vertices: 0,
9426                cycle_errors: 0,
9427                elapsed: start.elapsed(),
9428            });
9429        }
9430
9431        // Build demand subgraph with virtual edges
9432        let (precedents_to_eval, vdeps) = self.build_demand_subgraph(&target_vertex_ids);
9433
9434        if precedents_to_eval.is_empty() {
9435            return Ok(EvalResult {
9436                computed_vertices: 0,
9437                cycle_errors: 0,
9438                elapsed: start.elapsed(),
9439            });
9440        }
9441
9442        // Create schedule honoring virtual edges
9443        let scheduler = Scheduler::new(&self.graph);
9444        let schedule = scheduler.create_schedule_with_virtual(&precedents_to_eval, &vdeps)?;
9445
9446        // Handle cycles first
9447        let mut cycle_errors = 0;
9448        for cycle in &schedule.cycles {
9449            // Check cancellation between cycles
9450            if cancel_flag.load(Ordering::Relaxed) {
9451                return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
9452                    "Demand-driven evaluation cancelled during cycle handling".to_string(),
9453                ));
9454            }
9455
9456            cycle_errors += 1;
9457            let circ_error = LiteralValue::Error(
9458                ExcelError::new(ExcelErrorKind::Circ)
9459                    .with_message("Circular dependency detected".to_string()),
9460            );
9461            for &vertex_id in cycle {
9462                self.graph
9463                    .update_vertex_value(vertex_id, circ_error.clone());
9464                self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
9465            }
9466        }
9467
9468        // Evaluate layers with cancellation checks
9469        let mut computed_vertices = 0;
9470        for layer in &schedule.layers {
9471            // Check cancellation between layers
9472            if cancel_flag.load(Ordering::Relaxed) {
9473                return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
9474                    "Demand-driven evaluation cancelled between layers".to_string(),
9475                ));
9476            }
9477
9478            // Evaluate vertices in this layer (parallel or sequential)
9479            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
9480                computed_vertices +=
9481                    self.evaluate_layer_parallel_cancellable(layer, cancel_flag)?;
9482            } else {
9483                computed_vertices +=
9484                    self.evaluate_layer_sequential_cancellable_demand_driven(layer, cancel_flag)?;
9485            }
9486        }
9487
9488        // Clear dirty flags for evaluated vertices
9489        self.graph.clear_dirty_flags(&precedents_to_eval);
9490
9491        // Re-dirty volatile vertices
9492        self.graph.redirty_volatiles();
9493
9494        Ok(EvalResult {
9495            computed_vertices,
9496            cycle_errors,
9497            elapsed: start.elapsed(),
9498        })
9499    }
9500
9501    fn parse_a1_notation(&self, address: &str) -> Result<(String, u32, u32), ExcelError> {
9502        let mut parts = address.splitn(2, '!');
9503        let first = parts.next().unwrap_or_default();
9504        let remainder = parts.next();
9505
9506        let (sheet, cell_part) = match remainder {
9507            Some(cell) => (first.to_string(), cell),
9508            None => (self.default_sheet_name().to_string(), first),
9509        };
9510
9511        let (row, col, _, _) = parse_a1_1based(cell_part).map_err(|err| {
9512            ExcelError::new(ExcelErrorKind::Ref)
9513                .with_message(format!("Invalid cell reference `{cell_part}`: {err}"))
9514        })?;
9515
9516        Ok((sheet, row, col))
9517    }
9518
9519    /// Determine volatility using this engine's FunctionProvider, falling back to global registry.
9520    fn is_ast_volatile_with_provider(&self, ast: &ASTNode) -> bool {
9521        use formualizer_parse::parser::ASTNodeType;
9522        match &ast.node_type {
9523            ASTNodeType::Function { name, args, .. } => {
9524                if let Some(func) = self
9525                    .get_function("", name)
9526                    .or_else(|| crate::function_registry::get("", name))
9527                    && func.caps().contains(crate::function::FnCaps::VOLATILE)
9528                {
9529                    return true;
9530                }
9531                args.iter()
9532                    .any(|arg| self.is_ast_volatile_with_provider(arg))
9533            }
9534            ASTNodeType::BinaryOp { left, right, .. } => {
9535                self.is_ast_volatile_with_provider(left)
9536                    || self.is_ast_volatile_with_provider(right)
9537            }
9538            ASTNodeType::UnaryOp { expr, .. } => self.is_ast_volatile_with_provider(expr),
9539            ASTNodeType::Array(rows) => rows.iter().any(|row| {
9540                row.iter()
9541                    .any(|cell| self.is_ast_volatile_with_provider(cell))
9542            }),
9543            _ => false,
9544        }
9545    }
9546
9547    /// Find dirty precedents that need evaluation for the given target vertices
9548    fn find_dirty_precedents(&self, target_vertices: &[VertexId]) -> Vec<VertexId> {
9549        let mut to_evaluate = FxHashSet::default();
9550        let mut visited = FxHashSet::default();
9551        let mut stack = Vec::new();
9552
9553        // Start reverse traversal from target vertices
9554        for &target in target_vertices {
9555            stack.push(target);
9556        }
9557
9558        while let Some(vertex_id) = stack.pop() {
9559            if !visited.insert(vertex_id) {
9560                continue; // Already processed
9561            }
9562
9563            if self.graph.vertex_exists(vertex_id) {
9564                // Check if this vertex needs evaluation
9565                let kind = self.graph.get_vertex_kind(vertex_id);
9566                let needs_eval = match kind {
9567                    super::vertex::VertexKind::FormulaScalar
9568                    | super::vertex::VertexKind::FormulaArray => {
9569                        self.graph.is_dirty(vertex_id) || self.graph.is_volatile(vertex_id)
9570                    }
9571                    _ => false, // Values and empty cells don't need evaluation
9572                };
9573
9574                if needs_eval {
9575                    to_evaluate.insert(vertex_id);
9576                }
9577
9578                // Continue traversal to dependencies (precedents)
9579                if let Some(dependencies) = self.graph.dependencies_slice(vertex_id) {
9580                    for &dep_id in dependencies {
9581                        if !visited.contains(&dep_id) {
9582                            stack.push(dep_id);
9583                        }
9584                    }
9585                } else {
9586                    let dependencies = self.graph.get_dependencies(vertex_id);
9587                    for dep_id in dependencies {
9588                        if !visited.contains(&dep_id) {
9589                            stack.push(dep_id);
9590                        }
9591                    }
9592                }
9593            }
9594        }
9595
9596        let mut result: Vec<VertexId> = to_evaluate.into_iter().collect();
9597        result.sort_unstable();
9598        result
9599    }
9600
9601    /// Evaluate a layer sequentially
9602    fn evaluate_layer_sequential(
9603        &mut self,
9604        layer: &super::scheduler::Layer,
9605    ) -> Result<usize, ExcelError> {
9606        self.evaluate_layer_sequential_effects(layer)
9607    }
9608
9609    fn update_vertex_value_with_delta(
9610        &mut self,
9611        vertex_id: VertexId,
9612        new_value: LiteralValue,
9613        delta: &mut DeltaCollector,
9614    ) {
9615        if delta.mode != DeltaMode::Off
9616            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
9617        {
9618            let sheet_name = self.graph.sheet_name(cell.sheet_id);
9619            let old = self
9620                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
9621                .unwrap_or(LiteralValue::Empty);
9622            if old != new_value {
9623                delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
9624            }
9625        }
9626        self.graph.update_vertex_value(vertex_id, new_value.clone());
9627        self.mirror_vertex_value_to_overlay(vertex_id, &new_value);
9628    }
9629
9630    fn evaluate_layer_sequential_with_delta(
9631        &mut self,
9632        layer: &super::scheduler::Layer,
9633        delta: &mut DeltaCollector,
9634    ) -> Result<usize, ExcelError> {
9635        self.evaluate_layer_sequential_with_delta_effects(layer, delta)
9636    }
9637
9638    /// Evaluate a layer sequentially with cancellation support
9639    fn evaluate_layer_sequential_cancellable(
9640        &mut self,
9641        layer: &super::scheduler::Layer,
9642        cancel_flag: &AtomicBool,
9643    ) -> Result<usize, ExcelError> {
9644        self.evaluate_layer_sequential_cancellable_effects(layer, cancel_flag)
9645    }
9646
9647    /// Evaluate a layer sequentially with more frequent cancellation checks for demand-driven evaluation
9648    fn evaluate_layer_sequential_cancellable_demand_driven(
9649        &mut self,
9650        layer: &super::scheduler::Layer,
9651        cancel_flag: &AtomicBool,
9652    ) -> Result<usize, ExcelError> {
9653        self.evaluate_layer_sequential_cancellable_demand_driven_effects(layer, cancel_flag)
9654    }
9655
9656    /// Evaluate a layer in parallel using the thread pool
9657    fn evaluate_layer_parallel(
9658        &mut self,
9659        layer: &super::scheduler::Layer,
9660    ) -> Result<usize, ExcelError> {
9661        self.evaluate_layer_parallel_effects(layer)
9662    }
9663
9664    fn evaluate_layer_parallel_with_delta(
9665        &mut self,
9666        layer: &super::scheduler::Layer,
9667        delta: &mut DeltaCollector,
9668    ) -> Result<usize, ExcelError> {
9669        self.evaluate_layer_parallel_with_delta_effects(layer, delta)
9670    }
9671
9672    /// Evaluate a layer in parallel with cancellation support
9673    fn evaluate_layer_parallel_cancellable(
9674        &mut self,
9675        layer: &super::scheduler::Layer,
9676        cancel_flag: &AtomicBool,
9677    ) -> Result<usize, ExcelError> {
9678        self.evaluate_layer_parallel_cancellable_effects(layer, cancel_flag)
9679    }
9680
9681    /// Apply a computed result produced by `evaluate_vertex_immutable()`.
9682    ///
9683    /// This is the parallel equivalent of the "apply" portion of `evaluate_vertex_impl`.
9684    /// We keep apply sequential for correctness (spill commit is inherently stateful).
9685    fn apply_parallel_vertex_result(
9686        &mut self,
9687        vertex_id: VertexId,
9688        result: LiteralValue,
9689        mut delta: Option<&mut DeltaCollector>,
9690        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
9691    ) -> Result<(), ExcelError> {
9692        // If this vertex's cell is currently covered by a spill from a different anchor,
9693        // ignore the computed result. The spill's committed values own the grid.
9694        if let Some(cell) = self.graph.get_cell_ref(vertex_id)
9695            && let Some(owner) = self.graph.spill_registry_anchor_for_cell(cell)
9696            && owner != vertex_id
9697        {
9698            return Ok(());
9699        }
9700
9701        let kind = self.graph.get_vertex_kind(vertex_id);
9702
9703        // Only formula vertices spill dynamic arrays into the grid.
9704        let is_formula = matches!(kind, VertexKind::FormulaScalar | VertexKind::FormulaArray);
9705        if is_formula {
9706            match result {
9707                LiteralValue::Array(rows) => {
9708                    self.apply_array_result_from_parallel(
9709                        vertex_id,
9710                        rows,
9711                        delta.as_deref_mut(),
9712                        overwritable_formulas,
9713                    )?;
9714                }
9715                other => {
9716                    self.apply_non_array_result_from_parallel(
9717                        vertex_id,
9718                        other,
9719                        delta.as_deref_mut(),
9720                    );
9721                }
9722            }
9723            return Ok(());
9724        }
9725
9726        // Non-formula vertices: store value as-is (arrays remain arrays; no spill).
9727        if let Some(d) = delta {
9728            self.update_vertex_value_with_delta(vertex_id, result, d);
9729        } else {
9730            self.graph.update_vertex_value(vertex_id, result.clone());
9731            self.mirror_vertex_value_to_overlay(vertex_id, &result);
9732        }
9733        Ok(())
9734    }
9735
9736    fn apply_non_array_result_from_parallel(
9737        &mut self,
9738        vertex_id: VertexId,
9739        value: LiteralValue,
9740        delta: Option<&mut DeltaCollector>,
9741    ) {
9742        // Scalar/error result: store value and ensure any previous spill is cleared.
9743        // This mirrors the sequential behavior in `evaluate_vertex_impl`.
9744        let spill_cells = self
9745            .graph
9746            .spill_cells_for_anchor(vertex_id)
9747            .map(|cells| cells.to_vec())
9748            .unwrap_or_default();
9749
9750        if let Some(d) = delta
9751            && d.mode != DeltaMode::Off
9752            && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
9753        {
9754            if spill_cells.is_empty() {
9755                let old = self
9756                    .read_cell_value(
9757                        self.graph.sheet_name(anchor.sheet_id),
9758                        anchor.coord.row() + 1,
9759                        anchor.coord.col() + 1,
9760                    )
9761                    .unwrap_or(LiteralValue::Empty);
9762                if old != value {
9763                    d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
9764                }
9765            } else {
9766                for cell in spill_cells.iter() {
9767                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
9768                    let old = self
9769                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
9770                        .unwrap_or(LiteralValue::Empty);
9771                    let new = if cell.sheet_id == anchor.sheet_id
9772                        && cell.coord.row() == anchor.coord.row()
9773                        && cell.coord.col() == anchor.coord.col()
9774                    {
9775                        value.clone()
9776                    } else {
9777                        LiteralValue::Empty
9778                    };
9779                    Self::record_cell_if_changed(d, cell, &old, &new);
9780                }
9781            }
9782        }
9783
9784        self.graph.clear_spill_region(vertex_id);
9785        if let Some(scope) = Self::formula_plane_region_from_cells(&spill_cells) {
9786            self.record_formula_plane_structural_change(scope);
9787        }
9788
9789        if self.config.arrow_storage_enabled
9790            && self.config.delta_overlay_enabled
9791            && self.config.write_formula_overlay_enabled
9792        {
9793            let empty = LiteralValue::Empty;
9794            for cell in spill_cells.iter() {
9795                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
9796                self.mirror_value_to_computed_overlay(
9797                    &sheet_name,
9798                    cell.coord.row() + 1,
9799                    cell.coord.col() + 1,
9800                    &empty,
9801                );
9802            }
9803        }
9804
9805        self.graph.update_vertex_value(vertex_id, value.clone());
9806        self.mirror_vertex_value_to_overlay(vertex_id, &value);
9807    }
9808
9809    fn apply_array_result_from_parallel(
9810        &mut self,
9811        vertex_id: VertexId,
9812        rows: Vec<Vec<LiteralValue>>,
9813        mut delta: Option<&mut DeltaCollector>,
9814        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
9815    ) -> Result<(), ExcelError> {
9816        // Keep behavior consistent with the sequential spill path in `evaluate_vertex_impl`.
9817        self.graph
9818            .set_kind(vertex_id, crate::engine::vertex::VertexKind::FormulaArray);
9819
9820        let anchor = self
9821            .graph
9822            .get_cell_ref(vertex_id)
9823            .expect("cell ref for vertex");
9824        let sheet_id = anchor.sheet_id;
9825        let h = rows.len() as u32;
9826        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
9827
9828        // Hard cap to avoid vertex explosion from huge dynamic arrays.
9829        let spill_cells = (h as u64).saturating_mul(w as u64);
9830        if spill_cells > self.config.spill.max_spill_cells as u64 {
9831            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
9832            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
9833                .with_message("SpillTooLarge")
9834                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
9835                    expected_rows: h,
9836                    expected_cols: w,
9837                });
9838            let spill_val = LiteralValue::Error(spill_err.clone());
9839            if let Some(d) = delta.as_deref_mut()
9840                && d.mode != DeltaMode::Off
9841            {
9842                let old = self
9843                    .read_cell_value(
9844                        self.graph.sheet_name(anchor.sheet_id),
9845                        anchor.coord.row() + 1,
9846                        anchor.coord.col() + 1,
9847                    )
9848                    .unwrap_or(LiteralValue::Empty);
9849                if old != spill_val {
9850                    d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
9851                }
9852            }
9853            self.graph.update_vertex_value(vertex_id, spill_val.clone());
9854            self.mirror_vertex_value_to_overlay(vertex_id, &spill_val);
9855            return Ok(());
9856        }
9857
9858        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity)
9859        const PACKED_MAX_ROW: u32 = 1_048_575; // 20-bit max
9860        const PACKED_MAX_COL: u32 = 16_383; // 14-bit max
9861        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
9862        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
9863        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
9864            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
9865            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
9866                .with_message("Spill exceeds sheet bounds")
9867                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
9868                    expected_rows: h,
9869                    expected_cols: w,
9870                });
9871            let spill_val = LiteralValue::Error(spill_err.clone());
9872            if let Some(d) = delta.as_deref_mut()
9873                && d.mode != DeltaMode::Off
9874            {
9875                let old = self
9876                    .read_cell_value(
9877                        self.graph.sheet_name(anchor.sheet_id),
9878                        anchor.coord.row() + 1,
9879                        anchor.coord.col() + 1,
9880                    )
9881                    .unwrap_or(LiteralValue::Empty);
9882                if old != spill_val {
9883                    d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
9884                }
9885            }
9886            self.graph.update_vertex_value(vertex_id, spill_val.clone());
9887            self.mirror_vertex_value_to_overlay(vertex_id, &spill_val);
9888            return Ok(());
9889        }
9890
9891        let mut targets = Vec::new();
9892        for r in 0..h {
9893            for c in 0..w {
9894                targets.push(self.graph.make_cell_ref_internal(
9895                    sheet_id,
9896                    anchor.coord.row() + r,
9897                    anchor.coord.col() + c,
9898                ));
9899            }
9900        }
9901
9902        match self.spill_mgr.reserve(
9903            vertex_id,
9904            anchor,
9905            SpillShape { rows: h, cols: w },
9906            SpillMeta {
9907                epoch: self.recalc_epoch,
9908                config: self.config.spill,
9909            },
9910        ) {
9911            Ok(()) => {
9912                if let Err(e) = self.commit_spill_and_mirror(
9913                    vertex_id,
9914                    &targets,
9915                    rows.clone(),
9916                    delta.as_deref_mut(),
9917                    overwritable_formulas,
9918                ) {
9919                    self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
9920                    let err_val = LiteralValue::Error(e.clone());
9921                    if let Some(d) = delta.as_deref_mut()
9922                        && d.mode != DeltaMode::Off
9923                    {
9924                        let old = self
9925                            .read_cell_value(
9926                                self.graph.sheet_name(anchor.sheet_id),
9927                                anchor.coord.row() + 1,
9928                                anchor.coord.col() + 1,
9929                            )
9930                            .unwrap_or(LiteralValue::Empty);
9931                        if old != err_val {
9932                            d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
9933                        }
9934                    }
9935                    self.graph.update_vertex_value(vertex_id, err_val.clone());
9936                    self.mirror_vertex_value_to_overlay(vertex_id, &err_val);
9937                    return Ok(());
9938                }
9939
9940                // Anchor shows the top-left value, like Excel
9941                let top_left = rows
9942                    .first()
9943                    .and_then(|r| r.first())
9944                    .cloned()
9945                    .unwrap_or(LiteralValue::Empty);
9946                self.graph.update_vertex_value(vertex_id, top_left.clone());
9947                self.mirror_vertex_value_to_overlay(vertex_id, &top_left);
9948                Ok(())
9949            }
9950            Err(e) => {
9951                self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
9952                let spill_err = ExcelError::new(ExcelErrorKind::Spill)
9953                    .with_message(e.message.unwrap_or_else(|| "Spill blocked".to_string()))
9954                    .with_extra(formualizer_common::ExcelErrorExtra::Spill {
9955                        expected_rows: h,
9956                        expected_cols: w,
9957                    });
9958                let spill_val = LiteralValue::Error(spill_err.clone());
9959                if let Some(d) = delta
9960                    && d.mode != DeltaMode::Off
9961                {
9962                    let old = self
9963                        .read_cell_value(
9964                            self.graph.sheet_name(anchor.sheet_id),
9965                            anchor.coord.row() + 1,
9966                            anchor.coord.col() + 1,
9967                        )
9968                        .unwrap_or(LiteralValue::Empty);
9969                    if old != spill_val {
9970                        d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
9971                    }
9972                }
9973                self.graph.update_vertex_value(vertex_id, spill_val.clone());
9974                self.mirror_vertex_value_to_overlay(vertex_id, &spill_val);
9975                Ok(())
9976            }
9977        }
9978    }
9979
9980    /// Evaluate a single vertex without mutating the graph (for parallel evaluation)
9981    fn evaluate_vertex_immutable(&self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
9982        // Check if vertex exists
9983        if !self.graph.vertex_exists(vertex_id) {
9984            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
9985                .with_message(format!("Vertex not found: {vertex_id:?}")));
9986        }
9987
9988        // Get vertex kind and check if it needs evaluation
9989        let kind = self.graph.get_vertex_kind(vertex_id);
9990        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
9991
9992        let ast_id = match kind {
9993            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
9994                if let Some(ast_id) = self.graph.get_formula_id(vertex_id) {
9995                    ast_id
9996                } else {
9997                    return Ok(LiteralValue::Number(0.0));
9998                }
9999            }
10000            VertexKind::Empty | VertexKind::Cell => {
10001                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
10002                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
10003                    let row = cell_ref.coord.row() + 1;
10004                    let col = cell_ref.coord.col() + 1;
10005                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
10006                        return Ok(v);
10007                    }
10008                }
10009                return Ok(LiteralValue::Number(0.0));
10010            }
10011            VertexKind::NamedScalar => {
10012                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
10013                    ExcelError::new(ExcelErrorKind::Name)
10014                        .with_message("Named range metadata missing".to_string())
10015                })?;
10016
10017                return match &named_range.definition {
10018                    NamedDefinition::Cell(cell_ref) => {
10019                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
10020                        Ok(self
10021                            .get_cell_value(
10022                                sheet_name,
10023                                cell_ref.coord.row() + 1,
10024                                cell_ref.coord.col() + 1,
10025                            )
10026                            .unwrap_or(LiteralValue::Empty))
10027                    }
10028                    NamedDefinition::Literal(v) => Ok(v.clone()),
10029                    NamedDefinition::Formula { ast, .. } => {
10030                        let context_sheet = match named_range.scope {
10031                            NameScope::Sheet(id) => id,
10032                            NameScope::Workbook => sheet_id,
10033                        };
10034                        let sheet_name = self.graph.sheet_name(context_sheet);
10035                        let cell_ref = self
10036                            .graph
10037                            .get_cell_ref(vertex_id)
10038                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
10039                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
10040                        interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
10041                    }
10042                    NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
10043                        .with_message("Range-valued name evaluated as scalar".to_string())),
10044                };
10045            }
10046            VertexKind::NamedArray => {
10047                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
10048                    ExcelError::new(ExcelErrorKind::Name)
10049                        .with_message("Named range metadata missing".to_string())
10050                })?;
10051
10052                return match &named_range.definition {
10053                    NamedDefinition::Range(range_ref) => {
10054                        if range_ref.start.sheet_id != range_ref.end.sheet_id {
10055                            return Err(ExcelError::new(ExcelErrorKind::Ref)
10056                                .with_message("Named range cannot span sheets".to_string()));
10057                        }
10058                        let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
10059                        let sr0 = range_ref.start.coord.row();
10060                        let sc0 = range_ref.start.coord.col();
10061                        let er0 = range_ref.end.coord.row();
10062                        let ec0 = range_ref.end.coord.col();
10063                        if sr0 > er0 || sc0 > ec0 {
10064                            return Err(ExcelError::new(ExcelErrorKind::Ref)
10065                                .with_message("Invalid named range bounds".to_string()));
10066                        }
10067
10068                        let h = (er0 - sr0 + 1) as usize;
10069                        let w = (ec0 - sc0 + 1) as usize;
10070                        let cell_count = (h as u64).saturating_mul(w as u64);
10071                        if cell_count > self.config.spill.max_spill_cells as u64 {
10072                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
10073                                "Named range too large to materialize as an array".to_string(),
10074                            ));
10075                        }
10076
10077                        let mut rows = Vec::with_capacity(h);
10078                        for r0 in sr0..=er0 {
10079                            let mut row = Vec::with_capacity(w);
10080                            for c0 in sc0..=ec0 {
10081                                let v = self
10082                                    .get_cell_value(sheet_name, r0 + 1, c0 + 1)
10083                                    .unwrap_or(LiteralValue::Empty);
10084                                row.push(v);
10085                            }
10086                            rows.push(row);
10087                        }
10088                        Ok(LiteralValue::Array(rows))
10089                    }
10090                    NamedDefinition::Cell(cell_ref) => {
10091                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
10092                        let row = cell_ref.coord.row() + 1;
10093                        let col = cell_ref.coord.col() + 1;
10094                        let v = self
10095                            .get_cell_value(sheet_name, row, col)
10096                            .unwrap_or(LiteralValue::Empty);
10097                        Ok(LiteralValue::Array(vec![vec![v]]))
10098                    }
10099                    NamedDefinition::Literal(v) => Ok(LiteralValue::Array(vec![vec![v.clone()]])),
10100                    NamedDefinition::Formula { ast, .. } => {
10101                        let context_sheet = match named_range.scope {
10102                            NameScope::Sheet(id) => id,
10103                            NameScope::Workbook => sheet_id,
10104                        };
10105                        let sheet_name = self.graph.sheet_name(context_sheet);
10106                        let cell_ref = self
10107                            .graph
10108                            .get_cell_ref(vertex_id)
10109                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
10110                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
10111                        match interpreter.evaluate_ast(ast) {
10112                            Ok(cv) => {
10113                                let v = cv.into_literal();
10114                                match v {
10115                                    LiteralValue::Array(_) => Ok(v),
10116                                    other => Ok(LiteralValue::Array(vec![vec![other]])),
10117                                }
10118                            }
10119                            Err(err) => Ok(LiteralValue::Error(err)),
10120                        }
10121                    }
10122                };
10123            }
10124            VertexKind::InfiniteRange
10125            | VertexKind::Range
10126            | VertexKind::External
10127            | VertexKind::Table => {
10128                // Not directly evaluatable here.
10129                return Ok(LiteralValue::Number(0.0));
10130            }
10131        };
10132
10133        // The interpreter uses a reference to the engine as the context
10134        let sheet_name = self.graph.sheet_name(sheet_id);
10135        let cell_ref = self
10136            .graph
10137            .get_cell_ref(vertex_id)
10138            .expect("cell ref for vertex");
10139        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
10140
10141        interpreter
10142            .evaluate_arena_ast(ast_id, self.graph.data_store(), self.graph.sheet_reg())
10143            .map(|cv| cv.into_literal())
10144    }
10145
10146    /// Get access to the shared thread pool for parallel evaluation
10147    pub fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
10148        self.thread_pool.as_ref()
10149    }
10150}
10151
10152#[derive(Default)]
10153struct RowBoundsCache {
10154    snapshot: u64,
10155    // key: (sheet_id, col_idx)
10156    map: rustc_hash::FxHashMap<(u32, usize), (Option<u32>, Option<u32>)>,
10157}
10158
10159impl RowBoundsCache {
10160    fn new(snapshot: u64) -> Self {
10161        Self {
10162            snapshot,
10163            map: Default::default(),
10164        }
10165    }
10166    fn get_row_bounds(
10167        &self,
10168        sheet_id: SheetId,
10169        col_idx: usize,
10170        snapshot: u64,
10171    ) -> Option<(Option<u32>, Option<u32>)> {
10172        if self.snapshot != snapshot {
10173            return None;
10174        }
10175        self.map.get(&(sheet_id as u32, col_idx)).copied()
10176    }
10177    fn put_row_bounds(
10178        &mut self,
10179        sheet_id: SheetId,
10180        col_idx: usize,
10181        snapshot: u64,
10182        bounds: (Option<u32>, Option<u32>),
10183    ) {
10184        if self.snapshot != snapshot {
10185            self.snapshot = snapshot;
10186            self.map.clear();
10187        }
10188        self.map.insert((sheet_id as u32, col_idx), bounds);
10189    }
10190}
10191
10192struct UsedAxisBoundsCache {
10193    snapshot: u64,
10194    row_bounds_by_col_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
10195    col_bounds_by_row_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
10196    #[cfg(test)]
10197    row_hits: std::sync::atomic::AtomicUsize,
10198    #[cfg(test)]
10199    row_misses: std::sync::atomic::AtomicUsize,
10200    #[cfg(test)]
10201    col_hits: std::sync::atomic::AtomicUsize,
10202    #[cfg(test)]
10203    col_misses: std::sync::atomic::AtomicUsize,
10204}
10205
10206impl UsedAxisBoundsCache {
10207    fn new(snapshot: u64) -> Self {
10208        Self {
10209            snapshot,
10210            row_bounds_by_col_span: Default::default(),
10211            col_bounds_by_row_span: Default::default(),
10212            #[cfg(test)]
10213            row_hits: std::sync::atomic::AtomicUsize::new(0),
10214            #[cfg(test)]
10215            row_misses: std::sync::atomic::AtomicUsize::new(0),
10216            #[cfg(test)]
10217            col_hits: std::sync::atomic::AtomicUsize::new(0),
10218            #[cfg(test)]
10219            col_misses: std::sync::atomic::AtomicUsize::new(0),
10220        }
10221    }
10222
10223    fn reset_for_snapshot(&mut self, snapshot: u64) {
10224        if self.snapshot != snapshot {
10225            self.snapshot = snapshot;
10226            self.row_bounds_by_col_span.clear();
10227            self.col_bounds_by_row_span.clear();
10228        }
10229    }
10230
10231    fn get_row_bounds(
10232        &self,
10233        sheet_id: SheetId,
10234        start_col: u32,
10235        end_col: u32,
10236        snapshot: u64,
10237    ) -> Option<Option<(u32, u32)>> {
10238        if self.snapshot != snapshot {
10239            return None;
10240        }
10241        let cached = self
10242            .row_bounds_by_col_span
10243            .get(&(sheet_id, start_col, end_col))
10244            .copied();
10245        #[cfg(test)]
10246        if cached.is_some() {
10247            self.row_hits.fetch_add(1, Ordering::Relaxed);
10248        }
10249        cached
10250    }
10251
10252    fn put_row_bounds(
10253        &mut self,
10254        sheet_id: SheetId,
10255        start_col: u32,
10256        end_col: u32,
10257        snapshot: u64,
10258        bounds: Option<(u32, u32)>,
10259    ) {
10260        self.reset_for_snapshot(snapshot);
10261        self.row_bounds_by_col_span
10262            .insert((sheet_id, start_col, end_col), bounds);
10263        #[cfg(test)]
10264        self.row_misses.fetch_add(1, Ordering::Relaxed);
10265    }
10266
10267    fn get_col_bounds(
10268        &self,
10269        sheet_id: SheetId,
10270        start_row: u32,
10271        end_row: u32,
10272        snapshot: u64,
10273    ) -> Option<Option<(u32, u32)>> {
10274        if self.snapshot != snapshot {
10275            return None;
10276        }
10277        let cached = self
10278            .col_bounds_by_row_span
10279            .get(&(sheet_id, start_row, end_row))
10280            .copied();
10281        #[cfg(test)]
10282        if cached.is_some() {
10283            self.col_hits.fetch_add(1, Ordering::Relaxed);
10284        }
10285        cached
10286    }
10287
10288    fn put_col_bounds(
10289        &mut self,
10290        sheet_id: SheetId,
10291        start_row: u32,
10292        end_row: u32,
10293        snapshot: u64,
10294        bounds: Option<(u32, u32)>,
10295    ) {
10296        self.reset_for_snapshot(snapshot);
10297        self.col_bounds_by_row_span
10298            .insert((sheet_id, start_row, end_row), bounds);
10299        #[cfg(test)]
10300        self.col_misses.fetch_add(1, Ordering::Relaxed);
10301    }
10302}
10303
10304// Phase 2 shim: in-process spill manager delegating to current graph methods.
10305#[derive(Default)]
10306pub struct ShimSpillManager {
10307    region_locks: RegionLockManager,
10308    pub(crate) active_locks: rustc_hash::FxHashMap<VertexId, u64>,
10309}
10310
10311impl ShimSpillManager {
10312    pub(crate) fn reserve(
10313        &mut self,
10314        owner: VertexId,
10315        anchor_cell: CellRef,
10316        shape: SpillShape,
10317        _meta: SpillMeta,
10318    ) -> Result<(), ExcelError> {
10319        // Derive region from anchor + shape; enforce in-flight exclusivity only.
10320        let region = crate::engine::spill::Region {
10321            sheet_id: anchor_cell.sheet_id as u32,
10322            row_start: anchor_cell.coord.row(),
10323            row_end: anchor_cell
10324                .coord
10325                .row()
10326                .saturating_add(shape.rows)
10327                .saturating_sub(1),
10328            col_start: anchor_cell.coord.col(),
10329            col_end: anchor_cell
10330                .coord
10331                .col()
10332                .saturating_add(shape.cols)
10333                .saturating_sub(1),
10334        };
10335        match self.region_locks.reserve(region, owner) {
10336            Ok(id) => {
10337                if id != 0 {
10338                    self.active_locks.insert(owner, id);
10339                }
10340                Ok(())
10341            }
10342            Err(e) => Err(e),
10343        }
10344    }
10345
10346    pub(crate) fn commit_array_with_value_probe<F>(
10347        &mut self,
10348        graph: &mut DependencyGraph,
10349        anchor_vertex: VertexId,
10350        targets: &[CellRef],
10351        rows: Vec<Vec<LiteralValue>>,
10352        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
10353        mut value_probe: F,
10354    ) -> Result<(), ExcelError>
10355    where
10356        F: FnMut(&DependencyGraph, &CellRef) -> Option<LiteralValue>,
10357    {
10358        use formualizer_common::{ExcelErrorExtra, ExcelErrorKind};
10359
10360        // Re-run plan on concrete targets before committing to respect blockers.
10361        // This plan checks formula/spill ownership in the graph, but when the graph value cache
10362        // is disabled (Arrow-canonical mode), it cannot see non-empty value blockers.
10363        let plan_res = graph.plan_spill_region_allowing_formula_overwrite(
10364            anchor_vertex,
10365            targets,
10366            overwritable_formulas,
10367        );
10368        if let Err(e) = plan_res {
10369            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
10370                self.region_locks.release(id);
10371            }
10372            return Err(e);
10373        }
10374
10375        if !graph.value_cache_enabled() {
10376            // Compute expected spill shape from the target rectangle for diagnostics.
10377            let (expected_rows, expected_cols) = if targets.is_empty() {
10378                (0u32, 0u32)
10379            } else {
10380                let mut min_r = u32::MAX;
10381                let mut max_r = 0u32;
10382                let mut min_c = u32::MAX;
10383                let mut max_c = 0u32;
10384                for cell in targets {
10385                    let r = cell.coord.row();
10386                    let c = cell.coord.col();
10387                    min_r = min_r.min(r);
10388                    max_r = max_r.max(r);
10389                    min_c = min_c.min(c);
10390                    max_c = max_c.max(c);
10391                }
10392                (
10393                    max_r.saturating_sub(min_r).saturating_add(1),
10394                    max_c.saturating_sub(min_c).saturating_add(1),
10395                )
10396            };
10397
10398            let anchor_cell = graph
10399                .get_cell_ref(anchor_vertex)
10400                .expect("anchor cell ref for spill commit");
10401
10402            for cell in targets {
10403                // Never treat the anchor as a blocker.
10404                if *cell == anchor_cell {
10405                    continue;
10406                }
10407                // Skip cells already known to be owned by a spill; plan() handled spill conflicts.
10408                if graph.spill_registry_anchor_for_cell(*cell).is_some() {
10409                    continue;
10410                }
10411                // Skip formula vertices in the target region; plan() handled them (or allowed).
10412                if let Some(&vid) = graph.get_vertex_id_for_address(cell)
10413                    && vid != anchor_vertex
10414                {
10415                    match graph.get_vertex_kind(vid) {
10416                        crate::engine::vertex::VertexKind::FormulaScalar
10417                        | crate::engine::vertex::VertexKind::FormulaArray => {
10418                            // plan() already approved allowed overwrites.
10419                            continue;
10420                        }
10421                        _ => {}
10422                    }
10423                }
10424
10425                if let Some(v) = value_probe(graph, cell)
10426                    && !matches!(v, LiteralValue::Empty)
10427                {
10428                    if let Some(id) = self.active_locks.remove(&anchor_vertex) {
10429                        self.region_locks.release(id);
10430                    }
10431                    return Err(ExcelError::new(ExcelErrorKind::Spill)
10432                        .with_message("BlockedByValue")
10433                        .with_extra(ExcelErrorExtra::Spill {
10434                            expected_rows,
10435                            expected_cols,
10436                        }));
10437                }
10438            }
10439        }
10440
10441        let commit_res = graph.commit_spill_region_atomic_with_fault(
10442            anchor_vertex,
10443            targets.to_vec(),
10444            rows,
10445            None,
10446        );
10447        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
10448            self.region_locks.release(id);
10449        }
10450        commit_res.map(|_| ())
10451    }
10452
10453    /// Commit a spill and mirror all written cells into Arrow overlay via the owning engine.
10454    pub(crate) fn commit_array_with_overlay<R: EvaluationContext>(
10455        &mut self,
10456        engine: &mut Engine<R>,
10457        anchor_vertex: VertexId,
10458        targets: &[CellRef],
10459        rows: Vec<Vec<LiteralValue>>,
10460        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
10461    ) -> Result<(), ExcelError> {
10462        // Re-run plan on concrete targets before committing to respect blockers.
10463        let plan_res = engine.graph.plan_spill_region_allowing_formula_overwrite(
10464            anchor_vertex,
10465            targets,
10466            overwritable_formulas,
10467        );
10468        if let Err(e) = plan_res {
10469            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
10470                self.region_locks.release(id);
10471            }
10472            return Err(e);
10473        }
10474
10475        let commit_res = engine.graph.commit_spill_region_atomic_with_fault(
10476            anchor_vertex,
10477            targets.to_vec(),
10478            rows.clone(),
10479            None,
10480        );
10481        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
10482            self.region_locks.release(id);
10483        }
10484        commit_res.map(|_| ())?;
10485
10486        // Mirror into Arrow overlay when enabled
10487        if engine.config.arrow_storage_enabled
10488            && engine.config.delta_overlay_enabled
10489            && engine.config.write_formula_overlay_enabled
10490        {
10491            // Expect targets to be a contiguous rectangle row-major starting at some anchor
10492            for (idx, cell) in targets.iter().enumerate() {
10493                let (r_off, c_off) = {
10494                    if rows.is_empty() || rows[0].is_empty() {
10495                        (0usize, 0usize)
10496                    } else {
10497                        let width = rows[0].len();
10498                        (idx / width, idx % width)
10499                    }
10500                };
10501                let v = rows
10502                    .get(r_off)
10503                    .and_then(|r| r.get(c_off))
10504                    .cloned()
10505                    .unwrap_or(LiteralValue::Empty);
10506                let sheet_name = engine.graph.sheet_name(cell.sheet_id).to_string();
10507                engine.mirror_value_to_computed_overlay(
10508                    &sheet_name,
10509                    cell.coord.row() + 1,
10510                    cell.coord.col() + 1,
10511                    &v,
10512                );
10513            }
10514        }
10515        Ok(())
10516    }
10517}
10518
10519impl<R> Engine<R>
10520where
10521    R: EvaluationContext,
10522{
10523    fn resolve_shared_ref(
10524        &self,
10525        reference: &ReferenceType,
10526        current_sheet: &str,
10527    ) -> Result<formualizer_common::SheetRef<'static>, ExcelError> {
10528        use formualizer_common::{
10529            SheetCellRef as SharedCellRef, SheetLocator, SheetRangeRef as SharedRangeRef,
10530            SheetRef as SharedRef,
10531        };
10532
10533        // Preserve anchor flags from the parsed reference when possible.
10534        let sr = match reference {
10535            ReferenceType::Cell {
10536                sheet,
10537                row,
10538                col,
10539                row_abs,
10540                col_abs,
10541            } => {
10542                let row0 = row
10543                    .checked_sub(1)
10544                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10545                let col0 = col
10546                    .checked_sub(1)
10547                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10548                let sheet_loc = match sheet.as_deref() {
10549                    Some(name) => SheetLocator::from_name(name),
10550                    None => SheetLocator::Current,
10551                };
10552                let coord = formualizer_common::RelativeCoord::new(row0, col0, *row_abs, *col_abs);
10553                SharedRef::Cell(SharedCellRef::new(sheet_loc, coord))
10554            }
10555            ReferenceType::Range {
10556                sheet,
10557                start_row,
10558                start_col,
10559                end_row,
10560                end_col,
10561                start_row_abs,
10562                start_col_abs,
10563                end_row_abs,
10564                end_col_abs,
10565            } => {
10566                let sheet_loc = match sheet.as_deref() {
10567                    Some(name) => SheetLocator::from_name(name),
10568                    None => SheetLocator::Current,
10569                };
10570                let sr = start_row
10571                    .map(|r| {
10572                        r.checked_sub(1)
10573                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
10574                    })
10575                    .transpose()?;
10576                let sc = start_col
10577                    .map(|c| {
10578                        c.checked_sub(1)
10579                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
10580                    })
10581                    .transpose()?;
10582                let er = end_row
10583                    .map(|r| {
10584                        r.checked_sub(1)
10585                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
10586                    })
10587                    .transpose()?;
10588                let ec = end_col
10589                    .map(|c| {
10590                        c.checked_sub(1)
10591                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
10592                    })
10593                    .transpose()?;
10594                let range = SharedRangeRef::from_parts(
10595                    sheet_loc,
10596                    sr.map(|idx| formualizer_common::AxisBound::new(idx, *start_row_abs)),
10597                    sc.map(|idx| formualizer_common::AxisBound::new(idx, *start_col_abs)),
10598                    er.map(|idx| formualizer_common::AxisBound::new(idx, *end_row_abs)),
10599                    ec.map(|idx| formualizer_common::AxisBound::new(idx, *end_col_abs)),
10600                )
10601                .map_err(|_| ExcelError::new(ExcelErrorKind::Ref))?;
10602                SharedRef::Range(range)
10603            }
10604            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
10605        };
10606
10607        let current_id = self
10608            .graph
10609            .sheet_id(current_sheet)
10610            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10611
10612        let resolve_loc = |loc: SheetLocator<'_>| -> Result<SheetLocator<'static>, ExcelError> {
10613            match loc {
10614                SheetLocator::Current => Ok(SheetLocator::Id(current_id)),
10615                SheetLocator::Id(id) => Ok(SheetLocator::Id(id)),
10616                SheetLocator::Name(name) => {
10617                    let n = name.as_ref();
10618                    self.graph
10619                        .sheet_id(n)
10620                        .map(SheetLocator::Id)
10621                        .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
10622                }
10623            }
10624        };
10625
10626        match sr {
10627            SharedRef::Cell(cell) => {
10628                let owned = cell.into_owned();
10629                let sheet = resolve_loc(owned.sheet)?;
10630                Ok(SharedRef::Cell(SharedCellRef::new(sheet, owned.coord)))
10631            }
10632            SharedRef::Range(range) => {
10633                let owned = range.into_owned();
10634                let sheet = resolve_loc(owned.sheet)?;
10635                Ok(SharedRef::Range(SharedRangeRef {
10636                    sheet,
10637                    start_row: owned.start_row,
10638                    start_col: owned.start_col,
10639                    end_row: owned.end_row,
10640                    end_col: owned.end_col,
10641                }))
10642            }
10643        }
10644    }
10645}
10646
10647// Implement the resolver traits for the Engine.
10648// This allows the interpreter to resolve references by querying the engine's graph.
10649impl<R> crate::traits::ReferenceResolver for Engine<R>
10650where
10651    R: EvaluationContext,
10652{
10653    fn resolve_cell_reference(
10654        &self,
10655        sheet: Option<&str>,
10656        row: u32,
10657        col: u32,
10658    ) -> Result<LiteralValue, ExcelError> {
10659        let sheet_name = sheet.unwrap_or_else(|| self.default_sheet_name()); // FIXME: should use formula current-sheet context
10660        // Prefer engine's unified accessor which consults Arrow store for base values
10661        // and falls back to graph for formulas and stored values.
10662        if let Some(v) = self.get_cell_value(sheet_name, row, col) {
10663            Ok(v)
10664        } else {
10665            // Excel semantics: empty cell coerces to 0 in numeric contexts
10666            Ok(LiteralValue::Number(0.0))
10667        }
10668    }
10669}
10670
10671impl<R> crate::traits::RangeResolver for Engine<R>
10672where
10673    R: EvaluationContext,
10674{
10675    fn resolve_range_reference(
10676        &self,
10677        sheet: Option<&str>,
10678        sr: Option<u32>,
10679        sc: Option<u32>,
10680        er: Option<u32>,
10681        ec: Option<u32>,
10682    ) -> Result<Box<dyn crate::traits::Range>, ExcelError> {
10683        // For now, delegate range resolution to the external resolver.
10684        // A future optimization could be to handle this within the graph.
10685        self.resolver.resolve_range_reference(sheet, sr, sc, er, ec)
10686    }
10687}
10688
10689impl<R> crate::traits::NamedRangeResolver for Engine<R>
10690where
10691    R: EvaluationContext,
10692{
10693    fn resolve_named_range_reference(
10694        &self,
10695        name: &str,
10696    ) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
10697        self.resolver.resolve_named_range_reference(name)
10698    }
10699}
10700
10701impl<R> crate::traits::TableResolver for Engine<R>
10702where
10703    R: EvaluationContext,
10704{
10705    fn resolve_table_reference(
10706        &self,
10707        tref: &formualizer_parse::parser::TableReference,
10708    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
10709        self.resolver.resolve_table_reference(tref)
10710    }
10711}
10712
10713impl<R> crate::traits::SourceResolver for Engine<R>
10714where
10715    R: EvaluationContext,
10716{
10717    fn source_scalar_version(&self, name: &str) -> Option<u64> {
10718        self.resolver.source_scalar_version(name)
10719    }
10720
10721    fn resolve_source_scalar(&self, name: &str) -> Result<LiteralValue, ExcelError> {
10722        self.resolver.resolve_source_scalar(name)
10723    }
10724
10725    fn source_table_version(&self, name: &str) -> Option<u64> {
10726        self.resolver.source_table_version(name)
10727    }
10728
10729    fn resolve_source_table(
10730        &self,
10731        name: &str,
10732    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
10733        self.resolver.resolve_source_table(name)
10734    }
10735}
10736
10737// The Engine is a Resolver because it implements the constituent traits.
10738impl<R> crate::traits::Resolver for Engine<R> where R: EvaluationContext {}
10739
10740// The Engine provides functions by delegating to its internal resolver.
10741impl<R> crate::traits::FunctionProvider for Engine<R>
10742where
10743    R: EvaluationContext,
10744{
10745    fn get_function(
10746        &self,
10747        prefix: &str,
10748        name: &str,
10749    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
10750        self.resolver.get_function(prefix, name)
10751    }
10752}
10753
10754// Override EvaluationContext to provide thread pool access
10755impl<R> crate::traits::EvaluationContext for Engine<R>
10756where
10757    R: EvaluationContext,
10758{
10759    fn clock(&self) -> &dyn crate::timezone::ClockProvider {
10760        self.clock.as_ref()
10761    }
10762
10763    fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
10764        self.thread_pool.as_ref()
10765    }
10766
10767    fn cancellation_token(&self) -> Option<Arc<std::sync::atomic::AtomicBool>> {
10768        self.active_cancel_flag.clone()
10769    }
10770
10771    fn chunk_hint(&self) -> Option<usize> {
10772        // Use a simple heuristic from configuration (stripe width * height) as a default hint.
10773        let hint =
10774            (self.config.stripe_height as usize).saturating_mul(self.config.stripe_width as usize);
10775        Some(hint.clamp(1024, 1 << 20)) // clamp between 1K and ~1M
10776    }
10777
10778    fn volatile_level(&self) -> crate::traits::VolatileLevel {
10779        self.config.volatile_level
10780    }
10781
10782    fn workbook_seed(&self) -> u64 {
10783        self.config.workbook_seed
10784    }
10785
10786    fn recalc_epoch(&self) -> u64 {
10787        self.recalc_epoch
10788    }
10789
10790    fn workbook_sheet_count(&self) -> Option<usize> {
10791        Some(self.graph.sheet_reg().active_len())
10792    }
10793
10794    fn sheet_index_by_name(&self, sheet: &str) -> Option<usize> {
10795        self.graph.sheet_reg().active_position(sheet)
10796    }
10797
10798    fn current_sheet_index(&self, current_sheet: &str) -> Option<usize> {
10799        self.sheet_index_by_name(current_sheet)
10800    }
10801
10802    fn inspect_reference(
10803        &self,
10804        reference: &ReferenceType,
10805        current_sheet: &str,
10806    ) -> Result<Option<ReferenceInfo>, ExcelError> {
10807        let sheet_info = |sheet_name: &str| -> Result<(SheetId, usize), ExcelError> {
10808            let sheet_id = self
10809                .graph
10810                .sheet_id(sheet_name)
10811                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10812            let sheet_index = self
10813                .graph
10814                .sheet_reg()
10815                .active_position_by_id(sheet_id)
10816                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10817            Ok((sheet_id, sheet_index))
10818        };
10819
10820        let cell_info =
10821            |sheet_name: &str, row: u32, col: u32| -> Result<ReferenceInfo, ExcelError> {
10822                let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
10823                let row0 = row
10824                    .checked_sub(1)
10825                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10826                let col0 = col
10827                    .checked_sub(1)
10828                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10829                Ok(ReferenceInfo {
10830                    first_sheet_index: Some(sheet_index),
10831                    sheet_count: Some(1),
10832                    first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
10833                })
10834            };
10835
10836        let range_info = |sheet_name: &str,
10837                          start_row: Option<u32>,
10838                          start_col: Option<u32>|
10839         -> Result<ReferenceInfo, ExcelError> {
10840            let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
10841            let row = start_row.unwrap_or(1);
10842            let col = start_col.unwrap_or(1);
10843            let row0 = row
10844                .checked_sub(1)
10845                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10846            let col0 = col
10847                .checked_sub(1)
10848                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10849            Ok(ReferenceInfo {
10850                first_sheet_index: Some(sheet_index),
10851                sheet_count: Some(1),
10852                first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
10853            })
10854        };
10855
10856        let info = match reference {
10857            ReferenceType::Cell {
10858                sheet, row, col, ..
10859            } => {
10860                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
10861                cell_info(sheet_name, *row, *col)?
10862            }
10863            ReferenceType::Range {
10864                sheet,
10865                start_row,
10866                start_col,
10867                ..
10868            } => {
10869                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
10870                range_info(sheet_name, *start_row, *start_col)?
10871            }
10872            ReferenceType::Cell3D {
10873                sheet_first,
10874                sheet_last,
10875                row,
10876                col,
10877                ..
10878            } => {
10879                let first = cell_info(sheet_first, *row, *col)?;
10880                ReferenceInfo {
10881                    first_sheet_index: first.first_sheet_index,
10882                    sheet_count: self
10883                        .graph
10884                        .sheet_reg()
10885                        .active_span_len(sheet_first, sheet_last),
10886                    first_cell: first.first_cell,
10887                }
10888            }
10889            ReferenceType::Range3D {
10890                sheet_first,
10891                sheet_last,
10892                start_row,
10893                start_col,
10894                ..
10895            } => {
10896                let first = range_info(sheet_first, *start_row, *start_col)?;
10897                ReferenceInfo {
10898                    first_sheet_index: first.first_sheet_index,
10899                    sheet_count: self
10900                        .graph
10901                        .sheet_reg()
10902                        .active_span_len(sheet_first, sheet_last),
10903                    first_cell: first.first_cell,
10904                }
10905            }
10906            ReferenceType::NamedRange(name) => {
10907                let current_id = self
10908                    .graph
10909                    .sheet_id(current_sheet)
10910                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10911                let named = self
10912                    .graph
10913                    .resolve_name_entry(name, current_id)
10914                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10915                match &named.definition {
10916                    NamedDefinition::Cell(cell) => ReferenceInfo {
10917                        first_sheet_index: self
10918                            .graph
10919                            .sheet_reg()
10920                            .active_position_by_id(cell.sheet_id),
10921                        sheet_count: Some(1),
10922                        first_cell: Some(*cell),
10923                    },
10924                    NamedDefinition::Range(range) => ReferenceInfo {
10925                        first_sheet_index: self
10926                            .graph
10927                            .sheet_reg()
10928                            .active_position_by_id(range.start.sheet_id),
10929                        sheet_count: Some(1),
10930                        first_cell: Some(range.start),
10931                    },
10932                    NamedDefinition::Literal(_) | NamedDefinition::Formula { .. } => {
10933                        ReferenceInfo {
10934                            first_sheet_index: None,
10935                            sheet_count: None,
10936                            first_cell: None,
10937                        }
10938                    }
10939                }
10940            }
10941            ReferenceType::Table(tref) => {
10942                let table = self
10943                    .graph
10944                    .resolve_table_entry(&tref.name)
10945                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
10946                ReferenceInfo {
10947                    first_sheet_index: self
10948                        .graph
10949                        .sheet_reg()
10950                        .active_position_by_id(table.range.start.sheet_id),
10951                    sheet_count: Some(1),
10952                    first_cell: Some(table.range.start),
10953                }
10954            }
10955            ReferenceType::External(_) => return Err(ExcelError::new(ExcelErrorKind::Ref)),
10956        };
10957
10958        Ok(Some(info))
10959    }
10960
10961    fn formula_text_at_cell(&self, cell: CellRef) -> Result<Option<String>, ExcelError> {
10962        let sheet_name = self.graph.sheet_name(cell.sheet_id);
10963        if sheet_name.is_empty() {
10964            return Err(ExcelError::new(ExcelErrorKind::Ref));
10965        }
10966        let row = cell.coord.row() + 1;
10967        let col = cell.coord.col() + 1;
10968
10969        if let Some(entries) = self.staged_formulas.get(sheet_name)
10970            && let Some((_, _, text)) = entries
10971                .iter()
10972                .rev()
10973                .find(|(r, c, _)| *r == row && *c == col)
10974        {
10975            return Ok(Some(if text.starts_with('=') {
10976                text.clone()
10977            } else {
10978                format!("={text}")
10979            }));
10980        }
10981
10982        let Some(vertex) = self.graph.get_vertex_for_cell(&cell) else {
10983            return Ok(None);
10984        };
10985        let Some(ast) = self.graph.get_formula(vertex) else {
10986            return Ok(None);
10987        };
10988        Ok(Some(formualizer_parse::pretty::canonical_formula(&ast)))
10989    }
10990
10991    fn used_rows_for_columns(
10992        &self,
10993        sheet: &str,
10994        start_col: u32,
10995        end_col: u32,
10996    ) -> Option<(u32, u32)> {
10997        // Union Arrow-backed used-region with formula rows that have not been materialized yet.
10998        let sheet_id = self.graph.sheet_id(sheet)?;
10999        let snap = self.data_snapshot_id();
11000        if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
11001            guard
11002                .as_ref()
11003                .and_then(|cache| cache.get_row_bounds(sheet_id, start_col, end_col, snap))
11004        }) {
11005            return cached;
11006        }
11007
11008        let arrow_bounds = self
11009            .sheet_store()
11010            .sheet(sheet)
11011            .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
11012        let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
11013        let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
11014            Some(bounds)
11015        } else {
11016            let sc0 = start_col.saturating_sub(1);
11017            let ec0 = end_col.saturating_sub(1);
11018            self.graph
11019                .used_row_bounds_for_columns(sheet_id, sc0, ec0)
11020                .map(|(a0, b0)| (a0 + 1, b0 + 1))
11021        };
11022
11023        if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
11024            guard
11025                .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
11026                .put_row_bounds(sheet_id, start_col, end_col, snap, computed);
11027        }
11028
11029        computed
11030    }
11031
11032    fn used_cols_for_rows(&self, sheet: &str, start_row: u32, end_row: u32) -> Option<(u32, u32)> {
11033        // Union Arrow-backed used-region with formula columns that have not been materialized yet.
11034        let sheet_id = self.graph.sheet_id(sheet)?;
11035        let snap = self.data_snapshot_id();
11036        if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
11037            guard
11038                .as_ref()
11039                .and_then(|cache| cache.get_col_bounds(sheet_id, start_row, end_row, snap))
11040        }) {
11041            return cached;
11042        }
11043
11044        let arrow_bounds = self
11045            .sheet_store()
11046            .sheet(sheet)
11047            .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
11048        let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
11049        let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
11050            Some(bounds)
11051        } else {
11052            let sr0 = start_row.saturating_sub(1);
11053            let er0 = end_row.saturating_sub(1);
11054            self.graph
11055                .used_col_bounds_for_rows(sheet_id, sr0, er0)
11056                .map(|(a0, b0)| (a0 + 1, b0 + 1))
11057        };
11058
11059        if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
11060            guard
11061                .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
11062                .put_col_bounds(sheet_id, start_row, end_row, snap, computed);
11063        }
11064
11065        computed
11066    }
11067
11068    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
11069        let _ = self.graph.sheet_id(sheet)?;
11070        // Excel-like upper bounds; we expose something finite but large.
11071        // Backends may override with real bounds.
11072        Some((1_048_576, 16_384)) // 1048576 rows, 16384 cols (XFD)
11073    }
11074
11075    fn data_snapshot_id(&self) -> u64 {
11076        self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
11077    }
11078
11079    fn backend_caps(&self) -> crate::traits::BackendCaps {
11080        crate::traits::BackendCaps {
11081            streaming: true,
11082            used_region: true,
11083            write: false,
11084            tables: false,
11085            async_stream: false,
11086        }
11087    }
11088
11089    fn build_lookup_index(
11090        &self,
11091        view: &RangeView<'_>,
11092        axis: LookupAxis,
11093    ) -> Option<Arc<LookupIndex>> {
11094        self.build_lookup_index_impl(view, axis)
11095    }
11096
11097    // Flats removed
11098
11099    fn date_system(&self) -> crate::engine::DateSystem {
11100        self.config.date_system
11101    }
11102    /// New: resolve a reference into a RangeView (Phase 2 API)
11103    fn resolve_range_view<'c>(
11104        &'c self,
11105        reference: &ReferenceType,
11106        current_sheet: &str,
11107    ) -> Result<RangeView<'c>, ExcelError> {
11108        match reference {
11109            ReferenceType::External(ext) => {
11110                let name = ext.raw.as_str();
11111                match ext.kind {
11112                    formualizer_parse::parser::ExternalRefKind::Cell { .. } => {
11113                        let Some(source) = self.graph.resolve_source_scalar_entry(name) else {
11114                            return Err(ExcelError::new(ExcelErrorKind::Name)
11115                                .with_message(format!("Undefined name: {name}")));
11116                        };
11117                        let version = source
11118                            .version
11119                            .or_else(|| self.resolver.source_scalar_version(name));
11120                        let v = self.resolve_source_scalar_cached(name, version)?;
11121                        Ok(RangeView::from_owned_rows(
11122                            vec![vec![v]],
11123                            self.config.date_system,
11124                        ))
11125                    }
11126                    formualizer_parse::parser::ExternalRefKind::Range { .. } => {
11127                        let Some(source) = self.graph.resolve_source_table_entry(name) else {
11128                            return Err(ExcelError::new(ExcelErrorKind::Name)
11129                                .with_message(format!("Undefined table: {name}")));
11130                        };
11131                        let version = source
11132                            .version
11133                            .or_else(|| self.resolver.source_table_version(name));
11134                        let table = self.resolve_source_table_cached(name, version)?;
11135                        let spec = Some(formualizer_parse::parser::TableSpecifier::Data);
11136                        self.source_table_to_range_view(table.as_ref(), &spec)
11137                    }
11138                }
11139            }
11140            ReferenceType::Range { .. } => {
11141                let shared = self.resolve_shared_ref(reference, current_sheet)?;
11142                let formualizer_common::SheetRef::Range(range) = shared else {
11143                    return Err(ExcelError::new(ExcelErrorKind::Ref));
11144                };
11145                let sheet_id = match range.sheet {
11146                    formualizer_common::SheetLocator::Id(id) => id,
11147                    _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
11148                };
11149                let sheet_name = self.graph.sheet_name(sheet_id);
11150
11151                let bounded_range = if range.start_row.is_some()
11152                    && range.start_col.is_some()
11153                    && range.end_row.is_some()
11154                    && range.end_col.is_some()
11155                {
11156                    Some(RangeRef::try_from_shared(range.as_ref())?)
11157                } else {
11158                    None
11159                };
11160
11161                let mut sr = bounded_range
11162                    .as_ref()
11163                    .map(|r| r.start.coord.row() + 1)
11164                    .or_else(|| range.start_row.map(|b| b.index + 1));
11165                let mut sc = bounded_range
11166                    .as_ref()
11167                    .map(|r| r.start.coord.col() + 1)
11168                    .or_else(|| range.start_col.map(|b| b.index + 1));
11169                let mut er = bounded_range
11170                    .as_ref()
11171                    .map(|r| r.end.coord.row() + 1)
11172                    .or_else(|| range.end_row.map(|b| b.index + 1));
11173                let mut ec = bounded_range
11174                    .as_ref()
11175                    .map(|r| r.end.coord.col() + 1)
11176                    .or_else(|| range.end_col.map(|b| b.index + 1));
11177
11178                if sr.is_none() && er.is_none() {
11179                    // Full-column reference: anchor at row 1
11180                    let scv = sc.unwrap_or(1);
11181                    let ecv = ec.unwrap_or(scv);
11182                    sr = Some(1);
11183                    if let Some((_, max_r)) = self.used_rows_for_columns(sheet_name, scv, ecv) {
11184                        er = Some(max_r);
11185                    } else if let Some((max_rows, _)) = self.sheet_bounds(sheet_name) {
11186                        er = Some(self.config.max_open_ended_rows);
11187                    }
11188                }
11189                if sc.is_none() && ec.is_none() {
11190                    // Full-row reference: anchor at column 1
11191                    let srv = sr.unwrap_or(1);
11192                    let erv = er.unwrap_or(srv);
11193                    sc = Some(1);
11194                    if let Some((_, max_c)) = self.used_cols_for_rows(sheet_name, srv, erv) {
11195                        ec = Some(max_c);
11196                    } else if let Some((_, max_cols)) = self.sheet_bounds(sheet_name) {
11197                        ec = Some(self.config.max_open_ended_cols);
11198                    }
11199                }
11200                if sr.is_some() && er.is_none() {
11201                    let scv = sc.unwrap_or(1);
11202                    let ecv = ec.unwrap_or(scv);
11203                    if let Some((_, max_r)) = self.used_rows_for_columns(sheet_name, scv, ecv) {
11204                        er = Some(max_r);
11205                    } else if let Some((max_rows, _)) = self.sheet_bounds(sheet_name) {
11206                        er = Some(self.config.max_open_ended_rows);
11207                    }
11208                }
11209                if er.is_some() && sr.is_none() {
11210                    // Open start: anchor at row 1
11211                    sr = Some(1);
11212                }
11213                if sc.is_some() && ec.is_none() {
11214                    let srv = sr.unwrap_or(1);
11215                    let erv = er.unwrap_or(srv);
11216                    if let Some((_, max_c)) = self.used_cols_for_rows(sheet_name, srv, erv) {
11217                        ec = Some(max_c);
11218                    } else if let Some((_, max_cols)) = self.sheet_bounds(sheet_name) {
11219                        ec = Some(self.config.max_open_ended_cols);
11220                    }
11221                }
11222                if ec.is_some() && sc.is_none() {
11223                    // Open start: anchor at column 1
11224                    sc = Some(1);
11225                }
11226
11227                let sr = sr.unwrap_or(1);
11228                let sc = sc.unwrap_or(1);
11229                let er = er.unwrap_or(sr.saturating_sub(1));
11230                let ec = ec.unwrap_or(sc.saturating_sub(1));
11231
11232                if self.force_materialize_range_views {
11233                    if er < sr || ec < sc {
11234                        return Ok(RangeView::from_owned_rows(
11235                            Vec::new(),
11236                            self.config.date_system,
11237                        ));
11238                    }
11239                    let h = (er - sr + 1) as u64;
11240                    let w = (ec - sc + 1) as u64;
11241                    let cell_count = h.saturating_mul(w);
11242                    if cell_count <= self.config.spill.max_spill_cells as u64 {
11243                        let mut rows: Vec<Vec<LiteralValue>> = Vec::with_capacity(h as usize);
11244                        for r in sr..=er {
11245                            let mut rowv: Vec<LiteralValue> = Vec::with_capacity(w as usize);
11246                            for c in sc..=ec {
11247                                rowv.push(
11248                                    self.get_cell_value(sheet_name, r, c)
11249                                        .unwrap_or(LiteralValue::Empty),
11250                                );
11251                            }
11252                            rows.push(rowv);
11253                        }
11254                        return Ok(RangeView::from_owned_rows(rows, self.config.date_system));
11255                    }
11256                }
11257
11258                let Some(asheet) = self.sheet_store().sheet(sheet_name) else {
11259                    return Ok(RangeView::from_owned_rows(
11260                        Vec::new(),
11261                        self.config.date_system,
11262                    ));
11263                };
11264
11265                let rv = if er < sr || ec < sc {
11266                    asheet.range_view(1, 1, 0, 0)
11267                } else {
11268                    let sr0 = sr.saturating_sub(1) as usize;
11269                    let sc0 = sc.saturating_sub(1) as usize;
11270                    let er0 = er.saturating_sub(1) as usize;
11271                    let ec0 = ec.saturating_sub(1) as usize;
11272                    asheet.range_view(sr0, sc0, er0, ec0)
11273                };
11274
11275                Ok(rv)
11276            }
11277            ReferenceType::Cell { .. } => {
11278                let shared = self.resolve_shared_ref(reference, current_sheet)?;
11279                let formualizer_common::SheetRef::Cell(cell) = shared else {
11280                    return Err(ExcelError::new(ExcelErrorKind::Ref));
11281                };
11282                let addr = CellRef::try_from_shared(cell)?;
11283                let sheet_id = addr.sheet_id;
11284                let sheet_name = self.graph.sheet_name(sheet_id);
11285                let row = addr.coord.row() + 1;
11286                let col = addr.coord.col() + 1;
11287
11288                if self.force_materialize_range_views {
11289                    let v = self
11290                        .get_cell_value(sheet_name, row, col)
11291                        .unwrap_or(LiteralValue::Empty);
11292                    return Ok(RangeView::from_owned_rows(
11293                        vec![vec![v]],
11294                        self.config.date_system,
11295                    ));
11296                }
11297
11298                if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
11299                    let r0 = row.saturating_sub(1) as usize;
11300                    let c0 = col.saturating_sub(1) as usize;
11301                    let rv = asheet.range_view(r0, c0, r0, c0);
11302                    Ok(rv)
11303                } else {
11304                    let v = self
11305                        .get_cell_value(sheet_name, row, col)
11306                        .unwrap_or(LiteralValue::Empty);
11307                    Ok(RangeView::from_owned_rows(
11308                        vec![vec![v]],
11309                        self.config.date_system,
11310                    ))
11311                }
11312            }
11313            ReferenceType::NamedRange(name) => {
11314                if let Some(current_id) = self.graph.sheet_id(current_sheet)
11315                    && let Some(named) = self.graph.resolve_name_entry(name, current_id)
11316                {
11317                    match &named.definition {
11318                        NamedDefinition::Cell(cell_ref) => {
11319                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
11320                            if self.force_materialize_range_views {
11321                                let v = self
11322                                    .get_cell_value(
11323                                        sheet_name,
11324                                        cell_ref.coord.row() + 1,
11325                                        cell_ref.coord.col() + 1,
11326                                    )
11327                                    .unwrap_or(LiteralValue::Empty);
11328                                return Ok(RangeView::from_owned_rows(
11329                                    vec![vec![v]],
11330                                    self.config.date_system,
11331                                ));
11332                            } else {
11333                                let asheet = self
11334                                    .sheet_store()
11335                                    .sheet(sheet_name)
11336                                    .expect("Arrow sheet missing for named cell");
11337                                let r0 = cell_ref.coord.row() as usize;
11338                                let c0 = cell_ref.coord.col() as usize;
11339                                let rv = asheet.range_view(r0, c0, r0, c0);
11340                                return Ok(rv);
11341                            }
11342                        }
11343                        NamedDefinition::Range(range_ref) => {
11344                            let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
11345                            let sr = range_ref.start.coord.row() + 1;
11346                            let sc = range_ref.start.coord.col() + 1;
11347                            let er = range_ref.end.coord.row() + 1;
11348                            let ec = range_ref.end.coord.col() + 1;
11349                            if self.force_materialize_range_views {
11350                                let h = (er.saturating_sub(sr) + 1) as u64;
11351                                let w = (ec.saturating_sub(sc) + 1) as u64;
11352                                let cell_count = h.saturating_mul(w);
11353                                if cell_count <= self.config.spill.max_spill_cells as u64 {
11354                                    let mut rows: Vec<Vec<LiteralValue>> =
11355                                        Vec::with_capacity(h as usize);
11356                                    for r in sr..=er {
11357                                        let mut rowv: Vec<LiteralValue> =
11358                                            Vec::with_capacity(w as usize);
11359                                        for c in sc..=ec {
11360                                            rowv.push(
11361                                                self.get_cell_value(sheet_name, r, c)
11362                                                    .unwrap_or(LiteralValue::Empty),
11363                                            );
11364                                        }
11365                                        rows.push(rowv);
11366                                    }
11367                                    return Ok(RangeView::from_owned_rows(
11368                                        rows,
11369                                        self.config.date_system,
11370                                    ));
11371                                }
11372                            }
11373                            let asheet = self
11374                                .sheet_store()
11375                                .sheet(sheet_name)
11376                                .expect("Arrow sheet missing for named range");
11377                            let sr0 = range_ref.start.coord.row() as usize;
11378                            let sc0 = range_ref.start.coord.col() as usize;
11379                            let er0 = range_ref.end.coord.row() as usize;
11380                            let ec0 = range_ref.end.coord.col() as usize;
11381                            let rv = asheet.range_view(sr0, sc0, er0, ec0);
11382                            return Ok(rv);
11383                        }
11384                        NamedDefinition::Literal(v) => {
11385                            return Ok(RangeView::from_owned_rows(
11386                                vec![vec![v.clone()]],
11387                                self.config.date_system,
11388                            ));
11389                        }
11390                        NamedDefinition::Formula { .. } => {
11391                            if let Some(value) = self.graph.get_value(named.vertex) {
11392                                return Ok(RangeView::from_owned_rows(
11393                                    vec![vec![value]],
11394                                    self.config.date_system,
11395                                ));
11396                            }
11397                        }
11398                    }
11399                }
11400
11401                if let Some(source) = self.graph.resolve_source_scalar_entry(name) {
11402                    let version = source
11403                        .version
11404                        .or_else(|| self.resolver.source_scalar_version(name));
11405                    let v = self.resolve_source_scalar_cached(name, version)?;
11406                    return Ok(RangeView::from_owned_rows(
11407                        vec![vec![v]],
11408                        self.config.date_system,
11409                    ));
11410                }
11411
11412                let data = self.resolver.resolve_named_range_reference(name)?;
11413                Ok(RangeView::from_owned_rows(data, self.config.date_system))
11414            }
11415            ReferenceType::Table(tref) => {
11416                if let Some(table) = self.graph.resolve_table_entry(&tref.name) {
11417                    let sheet_name = self.graph.sheet_name(table.range.start.sheet_id);
11418                    let asheet = self
11419                        .sheet_store()
11420                        .sheet(sheet_name)
11421                        .expect("Arrow sheet missing for table reference");
11422
11423                    let sr0 = table.range.start.coord.row() as usize;
11424                    let sc0 = table.range.start.coord.col() as usize;
11425                    let er0 = table.range.end.coord.row() as usize;
11426                    let ec0 = table.range.end.coord.col() as usize;
11427
11428                    let has_totals = table.totals_row;
11429                    let has_headers = table.header_row;
11430                    let data_sr = if has_headers {
11431                        sr0.saturating_add(1)
11432                    } else {
11433                        sr0
11434                    };
11435                    let data_er = if has_totals {
11436                        er0.saturating_sub(1)
11437                    } else {
11438                        er0
11439                    };
11440
11441                    let select = |sr: usize, sc: usize, er: usize, ec: usize| {
11442                        if sr > er || sc > ec {
11443                            asheet.range_view(1, 1, 0, 0)
11444                        } else {
11445                            asheet.range_view(sr, sc, er, ec)
11446                        }
11447                    };
11448
11449                    let av = match &tref.specifier {
11450                        None => {
11451                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
11452                                "Table reference without specifier is unsupported".to_string(),
11453                            ));
11454                        }
11455                        Some(formualizer_parse::parser::TableSpecifier::Column(col)) => {
11456                            let Some(idx) = table.col_index(col) else {
11457                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
11458                                    "Column refers to unknown table column".to_string(),
11459                                ));
11460                            };
11461                            let c0 = sc0 + idx;
11462                            select(data_sr, c0, data_er, c0)
11463                        }
11464                        Some(formualizer_parse::parser::TableSpecifier::ColumnRange(
11465                            start,
11466                            end,
11467                        )) => {
11468                            let Some(si) = table.col_index(start) else {
11469                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
11470                                    "Column range refers to unknown column(s)".to_string(),
11471                                ));
11472                            };
11473                            let Some(ei) = table.col_index(end) else {
11474                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
11475                                    "Column range refers to unknown column(s)".to_string(),
11476                                ));
11477                            };
11478                            let (mut a, mut b) = (si, ei);
11479                            if a > b {
11480                                std::mem::swap(&mut a, &mut b);
11481                            }
11482                            let c_start = sc0 + a;
11483                            let c_end = sc0 + b;
11484                            select(data_sr, c_start, data_er, c_end)
11485                        }
11486                        Some(formualizer_parse::parser::TableSpecifier::All)
11487                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
11488                            formualizer_parse::parser::SpecialItem::All,
11489                        )) => select(sr0, sc0, er0, ec0),
11490                        Some(formualizer_parse::parser::TableSpecifier::Data)
11491                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
11492                            formualizer_parse::parser::SpecialItem::Data,
11493                        )) => select(data_sr, sc0, data_er, ec0),
11494                        Some(formualizer_parse::parser::TableSpecifier::Headers)
11495                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
11496                            formualizer_parse::parser::SpecialItem::Headers,
11497                        )) => {
11498                            if !has_headers {
11499                                asheet.range_view(1, 1, 0, 0)
11500                            } else {
11501                                select(sr0, sc0, sr0, ec0)
11502                            }
11503                        }
11504                        Some(formualizer_parse::parser::TableSpecifier::Totals)
11505                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
11506                            formualizer_parse::parser::SpecialItem::Totals,
11507                        )) => {
11508                            if !has_totals {
11509                                asheet.range_view(1, 1, 0, 0)
11510                            } else {
11511                                select(er0, sc0, er0, ec0)
11512                            }
11513                        }
11514                        Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
11515                            formualizer_parse::parser::SpecialItem::ThisRow,
11516                        )) => {
11517                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
11518                                "@ (This Row) requires table-aware context; not yet supported"
11519                                    .to_string(),
11520                            ));
11521                        }
11522                        Some(formualizer_parse::parser::TableSpecifier::Row(_))
11523                        | Some(formualizer_parse::parser::TableSpecifier::Combination(_)) => {
11524                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
11525                                "Complex structured references not yet supported".to_string(),
11526                            ));
11527                        }
11528                    };
11529
11530                    return Ok(av);
11531                }
11532
11533                if let Some(source) = self.graph.resolve_source_table_entry(&tref.name) {
11534                    let version = source
11535                        .version
11536                        .or_else(|| self.resolver.source_table_version(&tref.name));
11537                    let table = self.resolve_source_table_cached(&tref.name, version)?;
11538                    return self.source_table_to_range_view(table.as_ref(), &tref.specifier);
11539                }
11540
11541                // Fallback: materialize via Resolver::resolve_range_like tranche 1
11542                let boxed = self.resolve_range_like(&ReferenceType::Table(tref.clone()))?;
11543                let owned = boxed.materialise().into_owned();
11544                Ok(RangeView::from_owned_rows(owned, self.config.date_system))
11545            }
11546            ReferenceType::Cell3D { .. } | ReferenceType::Range3D { .. } => {
11547                Err(ExcelError::new(ExcelErrorKind::NImpl)
11548                    .with_message("3D references are not yet supported".to_string()))
11549            }
11550        }
11551    }
11552
11553    fn resolve_cell_reference_value(
11554        &self,
11555        sheet: Option<&str>,
11556        row: u32,
11557        col: u32,
11558        current_sheet: &str,
11559    ) -> Result<LiteralValue, ExcelError> {
11560        let sheet_name = sheet.unwrap_or(current_sheet);
11561        if self.graph.sheet_id(sheet_name).is_none() {
11562            return Err(ExcelError::new(ExcelErrorKind::Ref));
11563        }
11564        Ok(self
11565            .get_cell_value(sheet_name, row, col)
11566            .unwrap_or(LiteralValue::Empty))
11567    }
11568
11569    fn build_criteria_mask(
11570        &self,
11571        view: &RangeView<'_>,
11572        col_in_view: usize,
11573        pred: &crate::args::CriteriaPredicate,
11574    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
11575        if view.dims().1 == 0 {
11576            return None;
11577        }
11578        // If the view is logically open-ended but the backing sheet has no physical rows,
11579        // treat the mask as empty (0-len) rather than attempting to build a huge mask.
11580        let sheet_rows = view.sheet().nrows as usize;
11581        if sheet_rows == 0 || view.start_row() >= sheet_rows {
11582            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
11583        }
11584        compute_criteria_mask(view, col_in_view, pred)
11585    }
11586
11587    fn build_row_visibility_mask(
11588        &self,
11589        view: &RangeView<'_>,
11590        mode: VisibilityMaskMode,
11591    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
11592        self.build_row_visibility_mask_for_view(view, mode)
11593    }
11594}
11595
11596impl<R> Engine<R>
11597where
11598    R: EvaluationContext,
11599{
11600    fn clear_spill_projection_and_mirror(
11601        &mut self,
11602        anchor_vertex: VertexId,
11603        delta: Option<&mut DeltaCollector>,
11604    ) {
11605        let spill_cells = self
11606            .graph
11607            .spill_cells_for_anchor(anchor_vertex)
11608            .map(|cells| cells.to_vec())
11609            .unwrap_or_default();
11610        if spill_cells.is_empty() {
11611            return;
11612        }
11613
11614        if let Some(delta) = delta
11615            && delta.mode != DeltaMode::Off
11616        {
11617            let empty = LiteralValue::Empty;
11618            for cell in spill_cells.iter() {
11619                let sheet_name = self.graph.sheet_name(cell.sheet_id);
11620                let old = self
11621                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
11622                    .unwrap_or(LiteralValue::Empty);
11623                if old != empty {
11624                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
11625                }
11626            }
11627        }
11628
11629        self.graph.clear_spill_region(anchor_vertex);
11630        if let Some(scope) = Self::formula_plane_region_from_cells(&spill_cells) {
11631            self.record_formula_plane_structural_change(scope);
11632        }
11633
11634        if self.config.arrow_storage_enabled
11635            && self.config.delta_overlay_enabled
11636            && self.config.write_formula_overlay_enabled
11637        {
11638            let empty = LiteralValue::Empty;
11639            for cell in spill_cells.iter() {
11640                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
11641                self.mirror_value_to_computed_overlay(
11642                    &sheet_name,
11643                    cell.coord.row() + 1,
11644                    cell.coord.col() + 1,
11645                    &empty,
11646                );
11647            }
11648        }
11649    }
11650
11651    /// Helper: commit spill via shim and mirror resulting cells into Arrow overlay when enabled.
11652    fn commit_spill_and_mirror(
11653        &mut self,
11654        anchor_vertex: VertexId,
11655        targets: &[CellRef],
11656        rows: Vec<Vec<LiteralValue>>,
11657        delta: Option<&mut DeltaCollector>,
11658        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
11659    ) -> Result<(), ExcelError> {
11660        let prev_spill_cells = self
11661            .graph
11662            .spill_cells_for_anchor(anchor_vertex)
11663            .map(|cells| cells.to_vec())
11664            .unwrap_or_default();
11665
11666        if let Some(delta) = delta
11667            && delta.mode != DeltaMode::Off
11668        {
11669            let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
11670                targets.iter().copied().collect();
11671            let empty = LiteralValue::Empty;
11672
11673            // Clears (prev - targets)
11674            for cell in prev_spill_cells.iter() {
11675                if target_set.contains(cell) {
11676                    continue;
11677                }
11678                let sheet_name = self.graph.sheet_name(cell.sheet_id);
11679                let old = self
11680                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
11681                    .unwrap_or(LiteralValue::Empty);
11682                if old != empty {
11683                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
11684                }
11685            }
11686
11687            // Writes (targets)
11688            if !targets.is_empty() && !rows.is_empty() && !rows[0].is_empty() {
11689                let width = rows[0].len();
11690                for (idx, cell) in targets.iter().enumerate() {
11691                    let r_off = idx / width;
11692                    let c_off = idx % width;
11693                    let new = rows
11694                        .get(r_off)
11695                        .and_then(|r| r.get(c_off))
11696                        .cloned()
11697                        .unwrap_or(LiteralValue::Empty);
11698                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
11699                    let old = self
11700                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
11701                        .unwrap_or(LiteralValue::Empty);
11702                    if old != new {
11703                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
11704                    }
11705                }
11706            } else {
11707                // Degenerate shapes: if we have targets but no rows, treat as writing Empty.
11708                for cell in targets.iter() {
11709                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
11710                    let old = self
11711                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
11712                        .unwrap_or(LiteralValue::Empty);
11713                    if !matches!(old, LiteralValue::Empty) {
11714                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
11715                    }
11716                }
11717            }
11718        }
11719
11720        // Commit via shim (releases locks). When the graph value cache is disabled (Arrow-canonical
11721        // values), plan/commit must consult Arrow storage to detect non-empty value blockers.
11722        let arrow_sheets = &self.arrow_sheets;
11723        self.spill_mgr.commit_array_with_value_probe(
11724            &mut self.graph,
11725            anchor_vertex,
11726            targets,
11727            rows.clone(),
11728            overwritable_formulas,
11729            |g, cell| {
11730                let sheet_name = g.sheet_name(cell.sheet_id);
11731                let asheet = arrow_sheets.sheet(sheet_name)?;
11732                let r0 = cell.coord.row() as usize;
11733                let c0 = cell.coord.col() as usize;
11734                let v = asheet.get_cell_value(r0, c0);
11735                if matches!(v, LiteralValue::Empty) {
11736                    None
11737                } else {
11738                    Some(v)
11739                }
11740            },
11741        )?;
11742
11743        if let Some(scope) = Self::formula_plane_region_from_cells(&prev_spill_cells) {
11744            self.record_formula_plane_structural_change(scope);
11745        }
11746        if let Some(scope) = Self::formula_plane_region_from_cells(targets) {
11747            self.record_formula_plane_structural_change(scope);
11748        }
11749
11750        if self.config.arrow_storage_enabled
11751            && self.config.delta_overlay_enabled
11752            && self.config.write_formula_overlay_enabled
11753        {
11754            if !prev_spill_cells.is_empty() {
11755                let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
11756                    targets.iter().copied().collect();
11757                let empty = LiteralValue::Empty;
11758                for cell in prev_spill_cells.iter() {
11759                    if !target_set.contains(cell) {
11760                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
11761                        self.mirror_value_to_computed_overlay(
11762                            &sheet_name,
11763                            cell.coord.row() + 1,
11764                            cell.coord.col() + 1,
11765                            &empty,
11766                        );
11767                    }
11768                }
11769            }
11770
11771            for (idx, cell) in targets.iter().enumerate() {
11772                if rows.is_empty() || rows[0].is_empty() {
11773                    break;
11774                }
11775                let width = rows[0].len();
11776                let r_off = idx / width;
11777                let c_off = idx % width;
11778                let v = rows[r_off][c_off].clone();
11779                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
11780                self.mirror_value_to_computed_overlay(
11781                    &sheet_name,
11782                    cell.coord.row() + 1,
11783                    cell.coord.col() + 1,
11784                    &v,
11785                );
11786            }
11787        }
11788        Ok(())
11789    }
11790}
11791
11792// ── Effects pipeline (ticket 603) ──────────────────────────────────────────
11793//
11794// Compute → Plan → Apply separation for evaluation side-effects.
11795
11796use crate::engine::effects::Effect;
11797use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog, SpillSnapshot};
11798
11799impl<R> Engine<R>
11800where
11801    R: EvaluationContext,
11802{
11803    /// Plan effects for a single vertex after its value has been computed.
11804    ///
11805    /// This reads graph state but only performs lightweight mutations
11806    /// (`set_kind`, `spill_mgr.reserve`) that are needed for correctness
11807    /// during the planning phase.  Value-changing mutations are deferred to
11808    /// `apply_effect`.
11809    pub(crate) fn plan_vertex_effects(
11810        &mut self,
11811        vertex_id: VertexId,
11812        computed_value: LiteralValue,
11813        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
11814    ) -> Result<Vec<Effect>, ExcelError> {
11815        let kind = self.graph.get_vertex_kind(vertex_id);
11816        let is_formula = matches!(kind, VertexKind::FormulaScalar | VertexKind::FormulaArray);
11817
11818        // If this vertex's cell is currently covered by a spill from a different
11819        // anchor, ignore the computed result.  Formula vertices are exempt:
11820        // they must still evaluate so that overlapping spills produce #SPILL!.
11821        if !is_formula {
11822            if let Some(cell) = self.graph.get_cell_ref(vertex_id)
11823                && let Some(owner) = self.graph.spill_registry_anchor_for_cell(cell)
11824                && owner != vertex_id
11825            {
11826                return Ok(Vec::new());
11827            }
11828            // Non-formula vertices: store value as-is (arrays remain arrays; no spill).
11829            return Ok(vec![Effect::WriteCell {
11830                vertex_id,
11831                value: computed_value,
11832            }]);
11833        }
11834
11835        match computed_value {
11836            LiteralValue::Array(rows) => {
11837                self.plan_array_effects(vertex_id, rows, overwritable_formulas)
11838            }
11839            other => self.plan_scalar_effects(vertex_id, other),
11840        }
11841    }
11842
11843    /// Plan effects for a formula vertex that produced a scalar/error result.
11844    fn plan_scalar_effects(
11845        &self,
11846        vertex_id: VertexId,
11847        value: LiteralValue,
11848    ) -> Result<Vec<Effect>, ExcelError> {
11849        let has_spill = self
11850            .graph
11851            .spill_cells_for_anchor(vertex_id)
11852            .is_some_and(|c| !c.is_empty());
11853
11854        let mut effects = Vec::new();
11855        if has_spill {
11856            effects.push(Effect::SpillClear {
11857                anchor_vertex: vertex_id,
11858            });
11859        }
11860        effects.push(Effect::WriteCell { vertex_id, value });
11861        Ok(effects)
11862    }
11863
11864    /// Plan effects for a formula vertex that produced an array result.
11865    fn plan_array_effects(
11866        &mut self,
11867        vertex_id: VertexId,
11868        rows: Vec<Vec<LiteralValue>>,
11869        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
11870    ) -> Result<Vec<Effect>, ExcelError> {
11871        // Lightweight mutation needed for correct spill-blocking checks.
11872        self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
11873
11874        let anchor = self
11875            .graph
11876            .get_cell_ref(vertex_id)
11877            .expect("cell ref for vertex");
11878        let sheet_id = anchor.sheet_id;
11879        let h = rows.len() as u32;
11880        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
11881
11882        // Hard cap to avoid vertex explosion from huge dynamic arrays.
11883        let spill_cells = (h as u64).saturating_mul(w as u64);
11884        if spill_cells > self.config.spill.max_spill_cells as u64 {
11885            return self.plan_spill_error_effects(vertex_id, "SpillTooLarge", h, w);
11886        }
11887
11888        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity).
11889        const PACKED_MAX_ROW: u32 = 1_048_575;
11890        const PACKED_MAX_COL: u32 = 16_383;
11891        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
11892        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
11893        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
11894            return self.plan_spill_error_effects(vertex_id, "Spill exceeds sheet bounds", h, w);
11895        }
11896
11897        let mut targets = Vec::new();
11898        for r in 0..h {
11899            for c in 0..w {
11900                targets.push(self.graph.make_cell_ref_internal(
11901                    sheet_id,
11902                    anchor.coord.row() + r,
11903                    anchor.coord.col() + c,
11904                ));
11905            }
11906        }
11907
11908        // Region lock via spill manager.
11909        match self.spill_mgr.reserve(
11910            vertex_id,
11911            anchor,
11912            SpillShape { rows: h, cols: w },
11913            SpillMeta {
11914                epoch: self.recalc_epoch,
11915                config: self.config.spill,
11916            },
11917        ) {
11918            Ok(()) => {
11919                // Validate spill region is available.
11920                if let Err(_e) = self.graph.plan_spill_region_allowing_formula_overwrite(
11921                    vertex_id,
11922                    &targets,
11923                    overwritable_formulas,
11924                ) {
11925                    return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
11926                }
11927
11928                // Arrow-canonical mode: graph planning cannot see non-empty value blockers because
11929                // cell values are not cached in the dependency graph. Consult Arrow storage to
11930                // detect occupied cells in the target region.
11931                if !self.graph.value_cache_enabled() {
11932                    let sheet_name = self.graph.sheet_name(sheet_id);
11933                    if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
11934                        for cell in targets.iter() {
11935                            // Allow overwriting the anchor itself.
11936                            if *cell == anchor {
11937                                continue;
11938                            }
11939                            // Allow cells already owned by a spill (plan() validated spill ownership).
11940                            if self.graph.spill_registry_anchor_for_cell(*cell).is_some() {
11941                                continue;
11942                            }
11943                            // Skip formula blockers; plan() handled them (or allowed).
11944                            if let Some(&vid) = self.graph.get_vertex_id_for_address(cell)
11945                                && vid != vertex_id
11946                            {
11947                                match self.graph.get_vertex_kind(vid) {
11948                                    VertexKind::FormulaScalar | VertexKind::FormulaArray => {
11949                                        continue;
11950                                    }
11951                                    _ => {}
11952                                }
11953                            }
11954
11955                            let v = asheet.get_cell_value(
11956                                cell.coord.row() as usize,
11957                                cell.coord.col() as usize,
11958                            );
11959                            if !matches!(v, LiteralValue::Empty) {
11960                                return self.plan_spill_error_effects(
11961                                    vertex_id,
11962                                    "BlockedByValue",
11963                                    h,
11964                                    w,
11965                                );
11966                            }
11967                        }
11968                    }
11969                }
11970
11971                let top_left = rows
11972                    .first()
11973                    .and_then(|r| r.first())
11974                    .cloned()
11975                    .unwrap_or(LiteralValue::Empty);
11976
11977                let mut effects = Vec::new();
11978                // Clear previous spill if any.
11979                let has_prev = self
11980                    .graph
11981                    .spill_cells_for_anchor(vertex_id)
11982                    .is_some_and(|c| !c.is_empty());
11983                if has_prev {
11984                    effects.push(Effect::SpillClear {
11985                        anchor_vertex: vertex_id,
11986                    });
11987                }
11988                effects.push(Effect::SpillCommit {
11989                    anchor_vertex: vertex_id,
11990                    anchor_cell: anchor,
11991                    target_cells: targets,
11992                    values: rows,
11993                });
11994                effects.push(Effect::WriteCell {
11995                    vertex_id,
11996                    value: top_left,
11997                });
11998                Ok(effects)
11999            }
12000            Err(e) => {
12001                let msg = e.message.unwrap_or_else(|| "Spill blocked".to_string());
12002                self.plan_spill_error_effects(vertex_id, &msg, h, w)
12003            }
12004        }
12005    }
12006
12007    /// Build the effect list for a spill that failed validation.
12008    fn plan_spill_error_effects(
12009        &self,
12010        vertex_id: VertexId,
12011        message: &str,
12012        expected_rows: u32,
12013        expected_cols: u32,
12014    ) -> Result<Vec<Effect>, ExcelError> {
12015        let spill_err = ExcelError::new(ExcelErrorKind::Spill)
12016            .with_message(message)
12017            .with_extra(formualizer_common::ExcelErrorExtra::Spill {
12018                expected_rows,
12019                expected_cols,
12020            });
12021        let spill_val = LiteralValue::Error(spill_err);
12022
12023        let effects = vec![
12024            Effect::SpillClear {
12025                anchor_vertex: vertex_id,
12026            },
12027            Effect::WriteCell {
12028                vertex_id,
12029                value: spill_val,
12030            },
12031        ];
12032        Ok(effects)
12033    }
12034
12035    /// Apply a single effect, performing the actual graph mutations.
12036    pub(crate) fn apply_effect(
12037        &mut self,
12038        effect: &Effect,
12039        delta: Option<&mut DeltaCollector>,
12040        log: Option<&mut ChangeLog>,
12041    ) -> Result<(), ExcelError> {
12042        self.apply_effect_with_computed_writes(effect, delta, log, None)
12043    }
12044
12045    fn apply_effect_with_computed_writes(
12046        &mut self,
12047        effect: &Effect,
12048        delta: Option<&mut DeltaCollector>,
12049        log: Option<&mut ChangeLog>,
12050        computed_writes: Option<&mut ComputedWriteBuffer>,
12051    ) -> Result<(), ExcelError> {
12052        match effect {
12053            Effect::WriteCell { vertex_id, value } => {
12054                self.apply_write_cell(*vertex_id, value, delta, computed_writes)?;
12055            }
12056            Effect::SpillClear { anchor_vertex } => {
12057                self.apply_spill_clear(*anchor_vertex, delta, log, computed_writes)?;
12058            }
12059            Effect::SpillCommit {
12060                anchor_vertex,
12061                anchor_cell: _,
12062                target_cells,
12063                values,
12064            } => {
12065                self.apply_spill_commit(
12066                    *anchor_vertex,
12067                    target_cells,
12068                    values.clone(),
12069                    delta,
12070                    log,
12071                    computed_writes,
12072                )?;
12073            }
12074        }
12075        Ok(())
12076    }
12077
12078    /// Apply a WriteCell effect.
12079    fn apply_write_cell(
12080        &mut self,
12081        vertex_id: VertexId,
12082        value: &LiteralValue,
12083        delta: Option<&mut DeltaCollector>,
12084        mut computed_writes: Option<&mut ComputedWriteBuffer>,
12085    ) -> Result<(), ExcelError> {
12086        if let Some(d) = delta
12087            && d.mode != DeltaMode::Off
12088        {
12089            if let Some(buffer) = computed_writes.as_deref_mut() {
12090                self.flush_computed_write_buffer(buffer)?;
12091            }
12092            if let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id) {
12093                let sheet_name = self.graph.sheet_name(cell.sheet_id);
12094                let old = self
12095                    .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
12096                    .unwrap_or(LiteralValue::Empty);
12097                if old != *value {
12098                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
12099                }
12100            }
12101        }
12102        self.graph.update_vertex_value(vertex_id, value.clone());
12103        self.record_vertex_value_to_overlay(vertex_id, value, computed_writes)?;
12104        Ok(())
12105    }
12106
12107    /// Apply a SpillClear effect.
12108    fn apply_spill_clear(
12109        &mut self,
12110        anchor_vertex: VertexId,
12111        delta: Option<&mut DeltaCollector>,
12112        log: Option<&mut ChangeLog>,
12113        computed_writes: Option<&mut ComputedWriteBuffer>,
12114    ) -> Result<(), ExcelError> {
12115        if let Some(buffer) = computed_writes {
12116            self.flush_computed_write_buffer(buffer)?;
12117        }
12118
12119        let spill_cells = self
12120            .graph
12121            .spill_cells_for_anchor(anchor_vertex)
12122            .map(|cells| cells.to_vec())
12123            .unwrap_or_default();
12124        if spill_cells.is_empty() {
12125            return Ok(());
12126        }
12127
12128        // Snapshot for ChangeLog before clearing.
12129        let snapshot = if log.is_some() {
12130            self.snapshot_spill_for_anchor(anchor_vertex)
12131        } else {
12132            None
12133        };
12134
12135        // Record delta for cleared cells.
12136        if let Some(d) = delta
12137            && d.mode != DeltaMode::Off
12138        {
12139            let empty = LiteralValue::Empty;
12140            for cell in spill_cells.iter() {
12141                let sheet_name = self.graph.sheet_name(cell.sheet_id);
12142                let old = self
12143                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
12144                    .unwrap_or(LiteralValue::Empty);
12145                if old != empty {
12146                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
12147                }
12148            }
12149        }
12150
12151        self.graph.clear_spill_region(anchor_vertex);
12152        if let Some(scope) = Self::formula_plane_region_from_cells(&spill_cells) {
12153            self.record_formula_plane_structural_change(scope);
12154        }
12155
12156        // Mirror Empty to Arrow overlay for cleared cells.
12157        if self.config.arrow_storage_enabled
12158            && self.config.delta_overlay_enabled
12159            && self.config.write_formula_overlay_enabled
12160        {
12161            let empty = LiteralValue::Empty;
12162            for cell in spill_cells.iter() {
12163                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
12164                self.mirror_value_to_computed_overlay(
12165                    &sheet_name,
12166                    cell.coord.row() + 1,
12167                    cell.coord.col() + 1,
12168                    &empty,
12169                );
12170            }
12171        }
12172
12173        // ChangeLog.
12174        if let Some(log) = log
12175            && let Some(old) = snapshot
12176        {
12177            log.record(ChangeEvent::SpillCleared {
12178                anchor: anchor_vertex,
12179                old,
12180            });
12181        }
12182        Ok(())
12183    }
12184
12185    /// Apply a SpillCommit effect.
12186    fn apply_spill_commit(
12187        &mut self,
12188        anchor_vertex: VertexId,
12189        target_cells: &[CellRef],
12190        values: Vec<Vec<LiteralValue>>,
12191        delta: Option<&mut DeltaCollector>,
12192        log: Option<&mut ChangeLog>,
12193        computed_writes: Option<&mut ComputedWriteBuffer>,
12194    ) -> Result<(), ExcelError> {
12195        if let Some(buffer) = computed_writes {
12196            self.flush_computed_write_buffer(buffer)?;
12197        }
12198
12199        // Snapshot for ChangeLog before commit.
12200        let old_snapshot = if log.is_some() {
12201            self.snapshot_spill_for_anchor(anchor_vertex)
12202        } else {
12203            None
12204        };
12205
12206        // Delegate to existing commit_spill_and_mirror for delta + overlay logic.
12207        self.commit_spill_and_mirror(
12208            anchor_vertex,
12209            target_cells,
12210            values.clone(),
12211            delta,
12212            None, // overwritable_formulas already validated in plan phase
12213        )?;
12214
12215        // ChangeLog.
12216        if let Some(log) = log {
12217            log.record(ChangeEvent::SpillCommitted {
12218                anchor: anchor_vertex,
12219                old: old_snapshot,
12220                new: SpillSnapshot {
12221                    target_cells: target_cells.to_vec(),
12222                    values,
12223                },
12224            });
12225        }
12226        Ok(())
12227    }
12228
12229    /// Snapshot a spill region for ChangeLog recording.
12230    ///
12231    /// Extracted from `VertexEditor::snapshot_spill_for_anchor` to be usable
12232    /// without creating a `VertexEditor`.
12233    fn snapshot_spill_for_anchor(&self, anchor: VertexId) -> Option<SpillSnapshot> {
12234        let cells = self.graph.spill_cells_for_anchor(anchor)?.to_vec();
12235        if cells.is_empty() {
12236            return None;
12237        }
12238
12239        let max = self.config.spill.max_spill_cells as usize;
12240        let mut cells = cells;
12241        if cells.len() > max {
12242            cells.truncate(max);
12243        }
12244
12245        let first = *cells.first().expect("non-empty spill cells");
12246        let sheet_name = self.graph.sheet_name(first.sheet_id).to_string();
12247        let row0 = first.coord.row();
12248        let col0 = first.coord.col();
12249
12250        let mut max_row = row0;
12251        let mut max_col = col0;
12252        let mut by_coord: FxHashMap<(u32, u32), LiteralValue> = FxHashMap::default();
12253        for cell in &cells {
12254            max_row = max_row.max(cell.coord.row());
12255            max_col = max_col.max(cell.coord.col());
12256            let v = self
12257                .get_cell_value(&sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
12258                .unwrap_or(LiteralValue::Empty);
12259            by_coord.insert((cell.coord.row(), cell.coord.col()), v);
12260        }
12261
12262        let rows = (max_row - row0 + 1) as usize;
12263        let cols = (max_col - col0 + 1) as usize;
12264        let mut values: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
12265        for r in 0..rows {
12266            let mut row: Vec<LiteralValue> = Vec::with_capacity(cols);
12267            for c in 0..cols {
12268                row.push(
12269                    by_coord
12270                        .get(&(row0 + r as u32, col0 + c as u32))
12271                        .cloned()
12272                        .unwrap_or(LiteralValue::Empty),
12273                );
12274            }
12275            values.push(row);
12276        }
12277
12278        Some(SpillSnapshot {
12279            target_cells: cells,
12280            values,
12281        })
12282    }
12283
12284    fn flush_before_range_dependent_vertex(
12285        &mut self,
12286        vertex_id: VertexId,
12287        computed_writes: &mut ComputedWriteBuffer,
12288    ) -> Result<(), ExcelError> {
12289        if self.graph.get_range_dependencies(vertex_id).is_some() {
12290            self.flush_computed_write_buffer(computed_writes)?;
12291        }
12292        Ok(())
12293    }
12294
12295    fn plan_vertex_effects_with_computed_flush(
12296        &mut self,
12297        vertex_id: VertexId,
12298        computed_value: LiteralValue,
12299        overwritable_formulas: Option<&rustc_hash::FxHashSet<VertexId>>,
12300        computed_writes: &mut ComputedWriteBuffer,
12301    ) -> Result<Vec<Effect>, ExcelError> {
12302        if matches!(&computed_value, LiteralValue::Array(_)) {
12303            self.flush_computed_write_buffer(computed_writes)?;
12304        }
12305        self.plan_vertex_effects(vertex_id, computed_value, overwritable_formulas)
12306    }
12307
12308    // ── Layer evaluation via effects pipeline ──────────────────────────────
12309
12310    fn evaluate_small_layer_direct_effects(
12311        &mut self,
12312        layer: &super::scheduler::Layer,
12313        mut delta: Option<&mut DeltaCollector>,
12314        mut log: Option<&mut ChangeLog>,
12315        cancel_flag: Option<&AtomicBool>,
12316        cancel_check_every: usize,
12317        cancel_message: &'static str,
12318    ) -> Result<usize, ExcelError> {
12319        for (i, &vertex_id) in layer.vertices.iter().enumerate() {
12320            if cancel_check_every > 0
12321                && i % cancel_check_every == 0
12322                && cancel_flag.is_some_and(|flag| flag.load(Ordering::Relaxed))
12323            {
12324                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
12325                    .with_message(cancel_message.to_string()));
12326            }
12327            let value = match self.evaluate_vertex_immutable(vertex_id) {
12328                Ok(v) => v,
12329                Err(e) => LiteralValue::Error(e),
12330            };
12331            let effects = self.plan_vertex_effects(vertex_id, value, None)?;
12332            for effect in &effects {
12333                self.apply_effect_with_computed_writes(
12334                    effect,
12335                    delta.as_deref_mut(),
12336                    log.as_deref_mut(),
12337                    None,
12338                )?;
12339            }
12340        }
12341        Ok(layer.vertices.len())
12342    }
12343
12344    /// Evaluate a layer sequentially using the effects pipeline.
12345    fn evaluate_layer_sequential_effects(
12346        &mut self,
12347        layer: &super::scheduler::Layer,
12348    ) -> Result<usize, ExcelError> {
12349        if layer.vertices.len() < COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH {
12350            return self.evaluate_small_layer_direct_effects(
12351                layer,
12352                None,
12353                None,
12354                None,
12355                0,
12356                "Evaluation cancelled within layer",
12357            );
12358        }
12359
12360        let mut computed_writes = ComputedWriteBuffer::default();
12361        for &vertex_id in &layer.vertices {
12362            self.flush_before_range_dependent_vertex(vertex_id, &mut computed_writes)?;
12363            let value = match self.evaluate_vertex_immutable(vertex_id) {
12364                Ok(v) => v,
12365                Err(e) => LiteralValue::Error(e),
12366            };
12367            let effects = match self.plan_vertex_effects_with_computed_flush(
12368                vertex_id,
12369                value,
12370                None,
12371                &mut computed_writes,
12372            ) {
12373                Ok(effects) => effects,
12374                Err(e) => {
12375                    self.flush_computed_write_buffer(&mut computed_writes)?;
12376                    return Err(e);
12377                }
12378            };
12379            for effect in &effects {
12380                if let Err(e) = self.apply_effect_with_computed_writes(
12381                    effect,
12382                    None,
12383                    None,
12384                    Some(&mut computed_writes),
12385                ) {
12386                    self.flush_computed_write_buffer(&mut computed_writes)?;
12387                    return Err(e);
12388                }
12389            }
12390        }
12391        self.flush_computed_write_buffer(&mut computed_writes)?;
12392        Ok(layer.vertices.len())
12393    }
12394
12395    /// Evaluate a layer sequentially with delta collection via effects pipeline.
12396    fn evaluate_layer_sequential_with_delta_effects(
12397        &mut self,
12398        layer: &super::scheduler::Layer,
12399        delta: &mut DeltaCollector,
12400    ) -> Result<usize, ExcelError> {
12401        if layer.vertices.len() < COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH {
12402            return self.evaluate_small_layer_direct_effects(
12403                layer,
12404                Some(delta),
12405                None,
12406                None,
12407                0,
12408                "Evaluation cancelled within layer",
12409            );
12410        }
12411
12412        let mut computed_writes = ComputedWriteBuffer::default();
12413        for &vertex_id in &layer.vertices {
12414            self.flush_before_range_dependent_vertex(vertex_id, &mut computed_writes)?;
12415            let value = match self.evaluate_vertex_immutable(vertex_id) {
12416                Ok(v) => v,
12417                Err(e) => LiteralValue::Error(e),
12418            };
12419            let effects = match self.plan_vertex_effects_with_computed_flush(
12420                vertex_id,
12421                value,
12422                None,
12423                &mut computed_writes,
12424            ) {
12425                Ok(effects) => effects,
12426                Err(e) => {
12427                    self.flush_computed_write_buffer(&mut computed_writes)?;
12428                    return Err(e);
12429                }
12430            };
12431            for effect in &effects {
12432                if let Err(e) = self.apply_effect_with_computed_writes(
12433                    effect,
12434                    Some(delta),
12435                    None,
12436                    Some(&mut computed_writes),
12437                ) {
12438                    self.flush_computed_write_buffer(&mut computed_writes)?;
12439                    return Err(e);
12440                }
12441            }
12442        }
12443        self.flush_computed_write_buffer(&mut computed_writes)?;
12444        Ok(layer.vertices.len())
12445    }
12446
12447    /// Evaluate a layer sequentially with cancellation support via effects pipeline.
12448    fn evaluate_layer_sequential_cancellable_effects(
12449        &mut self,
12450        layer: &super::scheduler::Layer,
12451        cancel_flag: &AtomicBool,
12452    ) -> Result<usize, ExcelError> {
12453        if layer.vertices.len() < COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH {
12454            return self.evaluate_small_layer_direct_effects(
12455                layer,
12456                None,
12457                None,
12458                Some(cancel_flag),
12459                256,
12460                "Evaluation cancelled within layer",
12461            );
12462        }
12463
12464        let mut computed_writes = ComputedWriteBuffer::default();
12465        for (i, &vertex_id) in layer.vertices.iter().enumerate() {
12466            if i % 256 == 0 && cancel_flag.load(Ordering::Relaxed) {
12467                self.flush_computed_write_buffer(&mut computed_writes)?;
12468                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
12469                    .with_message("Evaluation cancelled within layer".to_string()));
12470            }
12471            self.flush_before_range_dependent_vertex(vertex_id, &mut computed_writes)?;
12472            let value = match self.evaluate_vertex_immutable(vertex_id) {
12473                Ok(v) => v,
12474                Err(e) => LiteralValue::Error(e),
12475            };
12476            let effects = match self.plan_vertex_effects_with_computed_flush(
12477                vertex_id,
12478                value,
12479                None,
12480                &mut computed_writes,
12481            ) {
12482                Ok(effects) => effects,
12483                Err(e) => {
12484                    self.flush_computed_write_buffer(&mut computed_writes)?;
12485                    return Err(e);
12486                }
12487            };
12488            for effect in &effects {
12489                if let Err(e) = self.apply_effect_with_computed_writes(
12490                    effect,
12491                    None,
12492                    None,
12493                    Some(&mut computed_writes),
12494                ) {
12495                    self.flush_computed_write_buffer(&mut computed_writes)?;
12496                    return Err(e);
12497                }
12498            }
12499        }
12500        self.flush_computed_write_buffer(&mut computed_writes)?;
12501        Ok(layer.vertices.len())
12502    }
12503
12504    /// Evaluate a layer sequentially with more frequent cancellation for demand-driven eval.
12505    fn evaluate_layer_sequential_cancellable_demand_driven_effects(
12506        &mut self,
12507        layer: &super::scheduler::Layer,
12508        cancel_flag: &AtomicBool,
12509    ) -> Result<usize, ExcelError> {
12510        if layer.vertices.len() < COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH {
12511            return self.evaluate_small_layer_direct_effects(
12512                layer,
12513                None,
12514                None,
12515                Some(cancel_flag),
12516                128,
12517                "Demand-driven evaluation cancelled within layer",
12518            );
12519        }
12520
12521        let mut computed_writes = ComputedWriteBuffer::default();
12522        for (i, &vertex_id) in layer.vertices.iter().enumerate() {
12523            if i % 128 == 0 && cancel_flag.load(Ordering::Relaxed) {
12524                self.flush_computed_write_buffer(&mut computed_writes)?;
12525                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
12526                    .with_message("Demand-driven evaluation cancelled within layer".to_string()));
12527            }
12528            self.flush_before_range_dependent_vertex(vertex_id, &mut computed_writes)?;
12529            let value = match self.evaluate_vertex_immutable(vertex_id) {
12530                Ok(v) => v,
12531                Err(e) => LiteralValue::Error(e),
12532            };
12533            let effects = match self.plan_vertex_effects_with_computed_flush(
12534                vertex_id,
12535                value,
12536                None,
12537                &mut computed_writes,
12538            ) {
12539                Ok(effects) => effects,
12540                Err(e) => {
12541                    self.flush_computed_write_buffer(&mut computed_writes)?;
12542                    return Err(e);
12543                }
12544            };
12545            for effect in &effects {
12546                if let Err(e) = self.apply_effect_with_computed_writes(
12547                    effect,
12548                    None,
12549                    None,
12550                    Some(&mut computed_writes),
12551                ) {
12552                    self.flush_computed_write_buffer(&mut computed_writes)?;
12553                    return Err(e);
12554                }
12555            }
12556        }
12557        self.flush_computed_write_buffer(&mut computed_writes)?;
12558        Ok(layer.vertices.len())
12559    }
12560
12561    /// Evaluate a layer in parallel, applying via effects pipeline.
12562    fn evaluate_layer_parallel_effects(
12563        &mut self,
12564        layer: &super::scheduler::Layer,
12565    ) -> Result<usize, ExcelError> {
12566        use rayon::prelude::*;
12567
12568        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
12569
12570        let mut phase1: Vec<VertexId> = Vec::new();
12571        let mut phase2: Vec<VertexId> = Vec::new();
12572        for &vid in &layer.vertices {
12573            if self.graph.get_range_dependencies(vid).is_some() {
12574                phase2.push(vid);
12575            } else {
12576                phase1.push(vid);
12577            }
12578        }
12579
12580        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
12581        let mut applied = 0usize;
12582
12583        for group in [&phase1[..], &phase2[..]] {
12584            if group.is_empty() {
12585                continue;
12586            }
12587            let mut computed_writes = ComputedWriteBuffer::default();
12588
12589            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
12590                thread_pool.install(|| {
12591                    group
12592                        .par_iter()
12593                        .map(
12594                            |&vertex_id| match self.evaluate_vertex_immutable(vertex_id) {
12595                                Ok(v) => Ok((vertex_id, v)),
12596                                Err(e) => Ok((vertex_id, LiteralValue::Error(e))),
12597                            },
12598                        )
12599                        .collect()
12600                });
12601
12602            match results {
12603                Ok(vertex_results) => {
12604                    // Arrays first, then scalars — establishes spill regions before
12605                    // scalar results that might land inside a spilled region.
12606                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
12607                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
12608                    for (vertex_id, result) in vertex_results {
12609                        if matches!(result, LiteralValue::Array(_)) {
12610                            arrays.push((vertex_id, result));
12611                        } else {
12612                            others.push((vertex_id, result));
12613                        }
12614                    }
12615                    for (vertex_id, result) in arrays {
12616                        let effects = match self.plan_vertex_effects_with_computed_flush(
12617                            vertex_id,
12618                            result,
12619                            Some(&inflight),
12620                            &mut computed_writes,
12621                        ) {
12622                            Ok(effects) => effects,
12623                            Err(e) => {
12624                                self.flush_computed_write_buffer(&mut computed_writes)?;
12625                                return Err(e);
12626                            }
12627                        };
12628                        for effect in &effects {
12629                            if let Err(e) = self.apply_effect_with_computed_writes(
12630                                effect,
12631                                None,
12632                                None,
12633                                Some(&mut computed_writes),
12634                            ) {
12635                                self.flush_computed_write_buffer(&mut computed_writes)?;
12636                                return Err(e);
12637                            }
12638                        }
12639                        applied = applied.saturating_add(1);
12640                    }
12641                    // Make all array spill/top-left writes visible before scalar effects in this group.
12642                    self.flush_computed_write_buffer(&mut computed_writes)?;
12643                    for (vertex_id, result) in others {
12644                        let effects = match self.plan_vertex_effects_with_computed_flush(
12645                            vertex_id,
12646                            result,
12647                            Some(&inflight),
12648                            &mut computed_writes,
12649                        ) {
12650                            Ok(effects) => effects,
12651                            Err(e) => {
12652                                self.flush_computed_write_buffer(&mut computed_writes)?;
12653                                return Err(e);
12654                            }
12655                        };
12656                        for effect in &effects {
12657                            if let Err(e) = self.apply_effect_with_computed_writes(
12658                                effect,
12659                                None,
12660                                None,
12661                                Some(&mut computed_writes),
12662                            ) {
12663                                self.flush_computed_write_buffer(&mut computed_writes)?;
12664                                return Err(e);
12665                            }
12666                        }
12667                        applied = applied.saturating_add(1);
12668                    }
12669                    // Flush at the group boundary; phase1 must be visible before phase2.
12670                    self.flush_computed_write_buffer(&mut computed_writes)?;
12671                }
12672                Err(e) => {
12673                    self.flush_computed_write_buffer(&mut computed_writes)?;
12674                    return Err(e);
12675                }
12676            }
12677        }
12678
12679        Ok(applied)
12680    }
12681
12682    /// Evaluate a layer in parallel with delta collection via effects pipeline.
12683    fn evaluate_layer_parallel_with_delta_effects(
12684        &mut self,
12685        layer: &super::scheduler::Layer,
12686        delta: &mut DeltaCollector,
12687    ) -> Result<usize, ExcelError> {
12688        use rayon::prelude::*;
12689
12690        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
12691
12692        let mut phase1: Vec<VertexId> = Vec::new();
12693        let mut phase2: Vec<VertexId> = Vec::new();
12694        for &vid in &layer.vertices {
12695            if self.graph.get_range_dependencies(vid).is_some() {
12696                phase2.push(vid);
12697            } else {
12698                phase1.push(vid);
12699            }
12700        }
12701
12702        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
12703        let mut applied = 0usize;
12704
12705        for group in [&phase1[..], &phase2[..]] {
12706            if group.is_empty() {
12707                continue;
12708            }
12709            let mut computed_writes = ComputedWriteBuffer::default();
12710            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
12711                thread_pool.install(|| {
12712                    group
12713                        .par_iter()
12714                        .map(
12715                            |&vertex_id| match self.evaluate_vertex_immutable(vertex_id) {
12716                                Ok(v) => Ok((vertex_id, v)),
12717                                Err(e) => Ok((vertex_id, LiteralValue::Error(e))),
12718                            },
12719                        )
12720                        .collect()
12721                });
12722
12723            match results {
12724                Ok(vertex_results) => {
12725                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
12726                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
12727                    for (vertex_id, result) in vertex_results {
12728                        if matches!(result, LiteralValue::Array(_)) {
12729                            arrays.push((vertex_id, result));
12730                        } else {
12731                            others.push((vertex_id, result));
12732                        }
12733                    }
12734                    for (vertex_id, result) in arrays {
12735                        let effects = match self.plan_vertex_effects_with_computed_flush(
12736                            vertex_id,
12737                            result,
12738                            Some(&inflight),
12739                            &mut computed_writes,
12740                        ) {
12741                            Ok(effects) => effects,
12742                            Err(e) => {
12743                                self.flush_computed_write_buffer(&mut computed_writes)?;
12744                                return Err(e);
12745                            }
12746                        };
12747                        for effect in &effects {
12748                            if let Err(e) = self.apply_effect_with_computed_writes(
12749                                effect,
12750                                Some(delta),
12751                                None,
12752                                Some(&mut computed_writes),
12753                            ) {
12754                                self.flush_computed_write_buffer(&mut computed_writes)?;
12755                                return Err(e);
12756                            }
12757                        }
12758                        applied = applied.saturating_add(1);
12759                    }
12760                    self.flush_computed_write_buffer(&mut computed_writes)?;
12761                    for (vertex_id, result) in others {
12762                        let effects = match self.plan_vertex_effects_with_computed_flush(
12763                            vertex_id,
12764                            result,
12765                            Some(&inflight),
12766                            &mut computed_writes,
12767                        ) {
12768                            Ok(effects) => effects,
12769                            Err(e) => {
12770                                self.flush_computed_write_buffer(&mut computed_writes)?;
12771                                return Err(e);
12772                            }
12773                        };
12774                        for effect in &effects {
12775                            if let Err(e) = self.apply_effect_with_computed_writes(
12776                                effect,
12777                                Some(delta),
12778                                None,
12779                                Some(&mut computed_writes),
12780                            ) {
12781                                self.flush_computed_write_buffer(&mut computed_writes)?;
12782                                return Err(e);
12783                            }
12784                        }
12785                        applied = applied.saturating_add(1);
12786                    }
12787                    self.flush_computed_write_buffer(&mut computed_writes)?;
12788                }
12789                Err(e) => {
12790                    self.flush_computed_write_buffer(&mut computed_writes)?;
12791                    return Err(e);
12792                }
12793            }
12794        }
12795
12796        Ok(applied)
12797    }
12798
12799    /// Evaluate a layer in parallel with cancellation support via effects pipeline.
12800    fn evaluate_layer_parallel_cancellable_effects(
12801        &mut self,
12802        layer: &super::scheduler::Layer,
12803        cancel_flag: &AtomicBool,
12804    ) -> Result<usize, ExcelError> {
12805        use rayon::prelude::*;
12806
12807        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
12808
12809        if cancel_flag.load(Ordering::Relaxed) {
12810            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
12811                .with_message("Parallel evaluation cancelled before starting".to_string()));
12812        }
12813
12814        let mut phase1: Vec<VertexId> = Vec::new();
12815        let mut phase2: Vec<VertexId> = Vec::new();
12816        for &vid in &layer.vertices {
12817            if self.graph.get_range_dependencies(vid).is_some() {
12818                phase2.push(vid);
12819            } else {
12820                phase1.push(vid);
12821            }
12822        }
12823
12824        let inflight: rustc_hash::FxHashSet<VertexId> = layer.vertices.iter().copied().collect();
12825        let mut applied = 0usize;
12826
12827        for group in [&phase1[..], &phase2[..]] {
12828            if group.is_empty() {
12829                continue;
12830            }
12831            let mut computed_writes = ComputedWriteBuffer::default();
12832
12833            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
12834                thread_pool.install(|| {
12835                    group
12836                        .par_iter()
12837                        .map(|&vertex_id| {
12838                            if cancel_flag.load(Ordering::Relaxed) {
12839                                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
12840                                    .with_message(
12841                                        "Parallel evaluation cancelled during execution"
12842                                            .to_string(),
12843                                    ));
12844                            }
12845                            match self.evaluate_vertex_immutable(vertex_id) {
12846                                Ok(v) => Ok((vertex_id, v)),
12847                                Err(e) => Ok((vertex_id, LiteralValue::Error(e))),
12848                            }
12849                        })
12850                        .collect()
12851                });
12852
12853            match results {
12854                Ok(vertex_results) => {
12855                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
12856                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
12857                    for (vertex_id, result) in vertex_results {
12858                        if matches!(result, LiteralValue::Array(_)) {
12859                            arrays.push((vertex_id, result));
12860                        } else {
12861                            others.push((vertex_id, result));
12862                        }
12863                    }
12864                    for (vertex_id, result) in arrays {
12865                        let effects = match self.plan_vertex_effects_with_computed_flush(
12866                            vertex_id,
12867                            result,
12868                            Some(&inflight),
12869                            &mut computed_writes,
12870                        ) {
12871                            Ok(effects) => effects,
12872                            Err(e) => {
12873                                self.flush_computed_write_buffer(&mut computed_writes)?;
12874                                return Err(e);
12875                            }
12876                        };
12877                        for effect in &effects {
12878                            if let Err(e) = self.apply_effect_with_computed_writes(
12879                                effect,
12880                                None,
12881                                None,
12882                                Some(&mut computed_writes),
12883                            ) {
12884                                self.flush_computed_write_buffer(&mut computed_writes)?;
12885                                return Err(e);
12886                            }
12887                        }
12888                        applied = applied.saturating_add(1);
12889                    }
12890                    self.flush_computed_write_buffer(&mut computed_writes)?;
12891                    for (vertex_id, result) in others {
12892                        let effects = match self.plan_vertex_effects_with_computed_flush(
12893                            vertex_id,
12894                            result,
12895                            Some(&inflight),
12896                            &mut computed_writes,
12897                        ) {
12898                            Ok(effects) => effects,
12899                            Err(e) => {
12900                                self.flush_computed_write_buffer(&mut computed_writes)?;
12901                                return Err(e);
12902                            }
12903                        };
12904                        for effect in &effects {
12905                            if let Err(e) = self.apply_effect_with_computed_writes(
12906                                effect,
12907                                None,
12908                                None,
12909                                Some(&mut computed_writes),
12910                            ) {
12911                                self.flush_computed_write_buffer(&mut computed_writes)?;
12912                                return Err(e);
12913                            }
12914                        }
12915                        applied = applied.saturating_add(1);
12916                    }
12917                    self.flush_computed_write_buffer(&mut computed_writes)?;
12918                }
12919                Err(e) => {
12920                    self.flush_computed_write_buffer(&mut computed_writes)?;
12921                    return Err(e);
12922                }
12923            }
12924        }
12925
12926        Ok(applied)
12927    }
12928
12929    // ── Top-level evaluate_all_logged ───────────────────────────────────────
12930
12931    /// Evaluate all dirty/volatile vertices, recording effects into a ChangeLog.
12932    ///
12933    /// This is the same flow as `evaluate_all` but threads a ChangeLog through
12934    /// every effect application so that spill commits/clears are captured.
12935    pub fn evaluate_all_logged(&mut self, log: &mut ChangeLog) -> Result<EvalResult, ExcelError> {
12936        let _source_cache = self.source_cache_session();
12937        self.validate_deterministic_mode()?;
12938        if self.config.defer_graph_building {
12939            self.build_graph_all()?;
12940        }
12941        if self.graph.formula_authority().active_span_count() > 0 {
12942            return self.evaluate_authoritative_formula_plane_all();
12943        }
12944        self.reset_virtual_dep_telemetry_if_disabled();
12945        let start = crate::instant::FzInstant::now();
12946        let mut computed_vertices = 0;
12947        let mut cycle_errors = 0;
12948
12949        let mut replan_iterations = 0;
12950        const MAX_REPLAN: usize = 5;
12951        let mut telemetry = self
12952            .config
12953            .enable_virtual_dep_telemetry
12954            .then(|| self.start_virtual_dep_telemetry());
12955
12956        log.begin_compound(format!("evaluate_all(epoch={})", self.recalc_epoch));
12957
12958        loop {
12959            let to_evaluate = self.graph.get_evaluation_vertices();
12960            if to_evaluate.is_empty() {
12961                if let Some(t) = telemetry.as_mut()
12962                    && t.bailout_reason.is_none()
12963                {
12964                    t.bailout_reason = Some("no_work");
12965                }
12966                break;
12967            }
12968
12969            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
12970            if let Some(t) = telemetry.as_mut() {
12971                Self::accumulate_schedule_meta(t, &meta);
12972            }
12973
12974            // Handle cycles.
12975            let circ_error = LiteralValue::Error(
12976                ExcelError::new(ExcelErrorKind::Circ)
12977                    .with_message("Circular dependency detected".to_string()),
12978            );
12979            for cycle in &schedule.cycles {
12980                cycle_errors += 1;
12981                for &vertex_id in cycle {
12982                    self.graph
12983                        .update_vertex_value(vertex_id, circ_error.clone());
12984                    self.mirror_vertex_value_to_overlay(vertex_id, &circ_error);
12985                }
12986            }
12987
12988            // Evaluate layers.
12989            for layer in &schedule.layers {
12990                computed_vertices += self.evaluate_layer_logged(layer, log)?;
12991            }
12992
12993            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
12994            if let Some(t) = telemetry.as_mut() {
12995                t.changed_vdeps_total += changed_vertices.len();
12996            }
12997            self.graph.clear_dirty_flags(&to_evaluate);
12998            for v in &changed_vertices {
12999                self.graph.set_dirty(*v, true);
13000            }
13001
13002            if changed_vertices.is_empty() {
13003                if let Some(t) = telemetry.as_mut() {
13004                    t.bailout_reason = Some("converged");
13005                }
13006                break;
13007            }
13008            if replan_iterations >= MAX_REPLAN {
13009                if let Some(t) = telemetry.as_mut() {
13010                    t.bailout_reason = Some("max_replan");
13011                }
13012                break;
13013            }
13014            replan_iterations += 1;
13015        }
13016
13017        if let Some(mut t) = telemetry {
13018            t.replan_iterations = replan_iterations;
13019            self.last_virtual_dep_telemetry = t;
13020        }
13021
13022        log.end_compound();
13023
13024        self.graph.redirty_volatiles();
13025        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
13026
13027        Ok(EvalResult {
13028            computed_vertices,
13029            cycle_errors,
13030            elapsed: start.elapsed(),
13031        })
13032    }
13033
13034    /// Evaluate a single layer with ChangeLog recording.
13035    fn evaluate_layer_logged(
13036        &mut self,
13037        layer: &super::scheduler::Layer,
13038        log: &mut ChangeLog,
13039    ) -> Result<usize, ExcelError> {
13040        let mut computed_writes = ComputedWriteBuffer::default();
13041        for &vertex_id in &layer.vertices {
13042            self.flush_before_range_dependent_vertex(vertex_id, &mut computed_writes)?;
13043            let value = match self.evaluate_vertex_immutable(vertex_id) {
13044                Ok(v) => v,
13045                Err(e) => LiteralValue::Error(e),
13046            };
13047            let effects = match self.plan_vertex_effects_with_computed_flush(
13048                vertex_id,
13049                value,
13050                None,
13051                &mut computed_writes,
13052            ) {
13053                Ok(effects) => effects,
13054                Err(e) => {
13055                    self.flush_computed_write_buffer(&mut computed_writes)?;
13056                    return Err(e);
13057                }
13058            };
13059            for effect in &effects {
13060                if let Err(e) = self.apply_effect_with_computed_writes(
13061                    effect,
13062                    None,
13063                    Some(log),
13064                    Some(&mut computed_writes),
13065                ) {
13066                    self.flush_computed_write_buffer(&mut computed_writes)?;
13067                    return Err(e);
13068                }
13069            }
13070        }
13071        self.flush_computed_write_buffer(&mut computed_writes)?;
13072        Ok(layer.vertices.len())
13073    }
13074}