Skip to main content

formualizer_eval/engine/
eval.rs

1use crate::SheetId;
2
3mod criteria;
4mod criteria_mask_cache;
5#[cfg(test)]
6mod criteria_mask_cache_tests;
7mod exact;
8mod family;
9mod kernels;
10mod lift;
11mod memo;
12use crate::arrow_store::{OverlayFragment, OverlayValue, SheetStore};
13#[cfg(test)]
14use crate::engine::Scheduler;
15use crate::engine::arena::AstNodeId;
16use crate::engine::eval_delta::{
17    DeltaCollector, DeltaMode, EvalDelta, EvalDeltaCompatibilityPolicy,
18};
19use crate::engine::graph::editor::change_log::MutationCapture;
20use crate::engine::graph::prepared_legacy_graph::PreparedLegacyGraphPlan;
21use crate::engine::ingest_pipeline::{DependencyPlanRow, FormulaAstInput};
22use crate::engine::live_edges::{LiveEdgeCollector, RecordingContext};
23use crate::engine::live_graph::analyze_live_graph;
24use crate::engine::lookup_index_cache::{
25    BuildOutcome, LookupAxis, LookupIndex, LookupIndexCache, LookupIndexCacheReport,
26    LookupIndexKey, estimate_bytes,
27};
28use crate::engine::named_range::{NameScope, NamedDefinition};
29use crate::engine::range_view::RangeView;
30use crate::engine::row_visibility::RowVisibilityState;
31use crate::engine::spill::{RegionLockManager, SpillMeta, SpillShape};
32use crate::engine::target_preparation::{
33    StagedFormulaIndex, StagedFormulaLease, StagedPackageLease,
34};
35use crate::engine::used_extent::{
36    ExtentPolicy, OpenRangeBounds, resolve_used_extent_with_fallback,
37};
38use crate::engine::virtual_deps::VirtualDepBuilder;
39use family::{LayerUnit, layer_units, unit_members};
40
41#[path = "freshness.rs"]
42mod freshness;
43use crate::engine::template::region::Region;
44use crate::engine::{
45    ChangeLogger, CycleDetection, CyclePolicy, DependencyGraph, EvalConfig, EvaluationRequestKind,
46    EvaluationRequestOutcome, EvaluationResourceBaselineStats, EvaluationResourceRequestStats,
47    FormulaDirtyLeaseOutcome, FormulaIngestBatch, FormulaIngestRecord, FormulaIngestReport,
48    FormulaParseDiagnostic, FormulaParsePolicy, FormulaPlaneMode, ResourceLedger,
49    RowVisibilitySource, ScheduleUnit, VertexId, VertexKind, VisibilityMaskMode,
50};
51use crate::function::FnCaps;
52use crate::interpreter::Interpreter;
53
54#[cfg(test)]
55mod fn_snapshot_tests;
56use crate::reference::{CellRef, Coord, RangeRef};
57use crate::traits::FunctionProvider;
58use crate::traits::{EvaluationContext, ReferenceInfo, Resolver};
59use formualizer_common::{
60    CoordBuildHasher, LiteralValue, col_letters_from_1based, parse_a1_1based,
61};
62use formualizer_parse::parser::ReferenceType;
63use formualizer_parse::{ASTNode, ASTNodeType, ExcelError, ExcelErrorKind};
64use rayon::ThreadPoolBuilder;
65use rustc_hash::{FxHashMap, FxHashSet};
66use std::collections::{BTreeMap, BTreeSet, VecDeque};
67use std::sync::Arc;
68use std::sync::atomic::{AtomicBool, Ordering};
69use std::time::Instant;
70
71type StagedFormulaEntry = (u32, u32, String);
72type StagedSheetParts = (
73    Vec<StagedFormulaEntry>,
74    Option<crate::engine::DeferredFormulaPackage>,
75);
76
77/// Per-sheet staged-formula store (NOTE(#126) follow-up).
78///
79/// Ingest consumers (`build_graph_all`/`build_graph_for_sheets`) walk staged
80/// entries in INSERTION order, so the order-preserving `Vec` stays the
81/// canonical storage; a `(row, col) → index` map removes the linear dup-scan
82/// that made `stage_formula_text`/`get_staged_formula_text` O(staged-on-sheet)
83/// per call (O(n²) for an n-formula deferred load on one sheet — ~570 ms for
84/// 50k stages, release, before the index). `stage`/`get` are O(1);
85/// `remove` keeps the old O(n) `Vec::remove` (rare path, order preserved).
86#[derive(Default)]
87pub(crate) struct StagedSheet {
88    entries: Vec<StagedFormulaEntry>,
89    index: FxHashMap<(u32, u32), usize>,
90    deferred_package: Option<crate::engine::DeferredFormulaPackage>,
91}
92
93impl StagedSheet {
94    fn invalidate_all_deferred_families(package: &mut crate::engine::DeferredFormulaPackage) {
95        package
96            .invalidated
97            .extend(package.families.iter().map(|family| family.source_id));
98        package.invalidated.extend(
99            package
100                .partitioned_families
101                .iter()
102                .map(|family| family.source_id),
103        );
104    }
105
106    fn invalidate_deferred_at(&mut self, row: u32, col: u32) {
107        let Some(package) = self.deferred_package.as_mut() else {
108            return;
109        };
110        if package.suppressed.contains(&(row, col)) {
111            // Previously consumed coordinates are already excluded from residual
112            // authority. Edits/deletion must not invalidate untouched fragments.
113            return;
114        }
115        if package.source_geometry_complete
116            && package.families.is_empty()
117            && package.partitioned_families.is_empty()
118        {
119            if let Some((row0, col0)) = row.checked_sub(1).zip(col.checked_sub(1))
120                && package
121                    .source_coordinates
122                    .binary_search(&crate::engine::SourceCoord {
123                        row: row0,
124                        col: col0,
125                    })
126                    .is_ok()
127            {
128                package.suppressed.insert((row, col));
129            }
130            return;
131        }
132        let family = package
133            .replay
134            .lock()
135            .ok()
136            .and_then(|mut replay| replay.formula_at(row, col).ok())
137            .flatten()
138            .and_then(|record| record.partition_owner.or(record.family));
139        if let Some(family) = family {
140            package.invalidated.insert(family);
141        } else {
142            // A poisoned lock, replay failure, malformed spool, or missing
143            // lookup result must never let a possibly edited family commit.
144            // Fail closed at package scope before suppressing the edited cell.
145            Self::invalidate_all_deferred_families(package);
146        }
147        package.suppressed.insert((row, col));
148    }
149
150    fn reconcile_attached_deferred_package(&mut self) {
151        let Some(package) = self.deferred_package.as_mut() else {
152            return;
153        };
154        if self.entries.is_empty() {
155            return;
156        }
157
158        let replayed = (|| {
159            let mut disposition = crate::engine::FormulaReplayDisposition::default();
160            for partition in &package.partitioned_families {
161                disposition.register_partition(partition, false)?;
162            }
163            package
164                .replay
165                .lock()
166                .map_err(|_| "deferred formula spool lock poisoned".to_string())?
167                .replay_partitioned(&disposition, &package.partitioned_families)
168        })();
169
170        let Ok(records) = replayed else {
171            Self::invalidate_all_deferred_families(package);
172            package
173                .suppressed
174                .extend(self.entries.iter().map(|(row, col, _)| (*row, *col)));
175            return;
176        };
177        let mut owners = FxHashMap::default();
178        for record in &records {
179            owners
180                .entry((record.row, record.col))
181                .or_insert(record.partition_owner.or(record.family));
182        }
183        for (row, col, _) in &self.entries {
184            if let Some(family) = owners.get(&(*row, *col)).copied().flatten() {
185                package.invalidated.insert(family);
186            } else {
187                Self::invalidate_all_deferred_families(package);
188            }
189            package.suppressed.insert((*row, *col));
190        }
191        package.reconciliation_replay = Some(records);
192    }
193
194    fn stage(&mut self, row: u32, col: u32, text: String) {
195        self.invalidate_deferred_at(row, col);
196        match self.index.entry((row, col)) {
197            std::collections::hash_map::Entry::Occupied(slot) => {
198                self.entries[*slot.get()].2 = text;
199            }
200            std::collections::hash_map::Entry::Vacant(slot) => {
201                slot.insert(self.entries.len());
202                self.entries.push((row, col, text));
203            }
204        }
205    }
206
207    fn remove(&mut self, row: u32, col: u32) -> Option<String> {
208        let deferred_text = self.get(row, col);
209        self.invalidate_deferred_at(row, col);
210        let Some(idx) = self.index.remove(&(row, col)) else {
211            return deferred_text;
212        };
213        let (_, _, text) = self.entries.remove(idx);
214        // `Vec::remove` shifted everything after `idx` left by one.
215        for slot in self.index.values_mut() {
216            if *slot > idx {
217                *slot -= 1;
218            }
219        }
220        Some(text)
221    }
222
223    fn get_ordinary(&self, row: u32, col: u32) -> Option<&str> {
224        self.index
225            .get(&(row, col))
226            .map(|&i| self.entries[i].2.as_str())
227    }
228
229    fn remove_ordinary(&mut self, row: u32, col: u32) -> Option<String> {
230        let idx = self.index.remove(&(row, col))?;
231        let (_, _, text) = self.entries.remove(idx);
232        for slot in self.index.values_mut() {
233            if *slot > idx {
234                *slot -= 1;
235            }
236        }
237        Some(text)
238    }
239
240    fn get(&self, row: u32, col: u32) -> Option<String> {
241        if let Some(text) = self.get_ordinary(row, col) {
242            return Some(text.to_string());
243        }
244        let package = self.deferred_package.as_ref()?;
245        if package.suppressed.contains(&(row, col)) {
246            return None;
247        }
248        package
249            .replay
250            .lock()
251            .ok()?
252            .formula_at(row, col)
253            .ok()?
254            .map(|record| record.text)
255    }
256
257    fn len(&self) -> usize {
258        self.deferred_package
259            .as_ref()
260            .map_or(self.entries.len(), |package| {
261                (if package.source_geometry_complete
262                    && (package.coordinates_cover_families
263                        || (package.families.is_empty() && package.partitioned_families.is_empty()))
264                {
265                    package.source_coordinates.len()
266                } else {
267                    usize::try_from(package.report.source_formula_records_spooled)
268                        .unwrap_or(usize::MAX)
269                })
270                .saturating_sub(package.suppressed.len())
271                .saturating_add(self.entries.len())
272            })
273    }
274
275    fn is_empty(&self) -> bool {
276        self.entries.is_empty() && self.deferred_package.is_none()
277    }
278
279    fn into_parts(self) -> StagedSheetParts {
280        (self.entries, self.deferred_package)
281    }
282}
283
284type StagedFormulaMap = std::collections::HashMap<String, StagedSheet>;
285
286#[derive(Clone, Debug, PartialEq, Eq, Hash)]
287struct PreparationRegion {
288    sheet: String,
289    sheet_id: SheetId,
290    start_row: u32,
291    start_col: u32,
292    end_row: u32,
293    end_col: u32,
294}
295
296// Target roots dedupe by identity; they keep their first-seen order.
297struct OrderedTargetProducers {
298    ordered: Vec<crate::engine::target_preparation::TargetProducer>,
299    seen: FxHashSet<crate::engine::target_preparation::TargetProducer>,
300}
301
302impl OrderedTargetProducers {
303    fn with_capacity(capacity: usize) -> Result<Self, std::collections::TryReserveError> {
304        let mut ordered = Vec::new();
305        ordered.try_reserve(capacity)?;
306        let mut seen = FxHashSet::default();
307        seen.try_reserve(capacity)?;
308        Ok(Self { ordered, seen })
309    }
310
311    fn from_ordered(
312        ordered: Vec<crate::engine::target_preparation::TargetProducer>,
313    ) -> Result<Self, std::collections::TryReserveError> {
314        let mut seen = FxHashSet::default();
315        seen.try_reserve(ordered.len())?;
316        seen.extend(ordered.iter().copied());
317        Ok(Self { ordered, seen })
318    }
319
320    fn push(
321        &mut self,
322        producer: crate::engine::target_preparation::TargetProducer,
323    ) -> Result<bool, std::collections::TryReserveError> {
324        #[cfg(test)]
325        TARGET_ROOT_DEDUP_PROBES.with(|probes| probes.set(probes.get().saturating_add(1)));
326        if self.seen.contains(&producer) {
327            return Ok(false);
328        }
329        self.seen.try_reserve(1)?;
330        self.ordered.try_reserve(1)?;
331        self.seen.insert(producer);
332        self.ordered.push(producer);
333        Ok(true)
334    }
335
336    fn len(&self) -> usize {
337        self.ordered.len()
338    }
339
340    fn into_vec(self) -> Vec<crate::engine::target_preparation::TargetProducer> {
341        self.ordered
342    }
343}
344
345#[cfg(test)]
346thread_local! {
347    static TARGET_ROOT_DEDUP_PROBES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
348}
349
350fn target_root_allocation_error(observed: usize, request_id: Option<u64>) -> ExcelError {
351    crate::engine::ResourceLedgerError::Exhausted(formualizer_common::ResourceExhaustionDetail {
352        reason: formualizer_common::ResourceExhaustionReason::ScratchMemory,
353        limit: u64::MAX,
354        observed: observed as u64,
355        request_id,
356    })
357    .into_excel_error()
358}
359
360#[derive(Debug)]
361struct PreparedOrdinaryStagedFormula {
362    sheet: String,
363    sheet_id: SheetId,
364    lease: StagedFormulaLease,
365    ast_id: Option<AstNodeId>,
366    plan: Option<DependencyPlanRow>,
367}
368
369struct PreparedTargetSourcePackage {
370    sheet: String,
371    sheet_id: SheetId,
372    lease: StagedPackageLease,
373    selected_points: Option<BTreeSet<(u32, u32)>>,
374    complete_selections: BTreeSet<crate::engine::SourceFamilyId>,
375    deferred_shared: bool,
376    direct_domains: Vec<(
377        crate::engine::SourceFamilyId,
378        crate::engine::PlacementDomainTransport,
379    )>,
380    source_report: crate::engine::FormulaCompressedSourceReport,
381    replay_records: Vec<crate::engine::DeferredReplayFormula>,
382    spool_replays: u64,
383    disposition: crate::engine::FormulaReplayDisposition,
384    legacy: Vec<(u32, u32, AstNodeId, DependencyPlanRow)>,
385    direct_families: usize,
386    direct_cells: u64,
387    direct_fragments: u64,
388    direct_complete_families: u64,
389    direct_complete_cells: u64,
390    direct_partition_families: u64,
391    direct_partition_cells: u64,
392    anchor_parses: u64,
393    anchor_asts: u64,
394    anchor_analyses: u64,
395}
396
397impl PreparedTargetSourcePackage {
398    fn empty_selection(sheet: &str, sheet_id: SheetId, lease: StagedPackageLease) -> Self {
399        Self {
400            sheet: sheet.to_owned(),
401            sheet_id,
402            lease,
403            selected_points: Some(BTreeSet::new()),
404            complete_selections: Default::default(),
405            deferred_shared: false,
406            direct_domains: Vec::new(),
407            source_report: Default::default(),
408            replay_records: Vec::new(),
409            spool_replays: 0,
410            disposition: Default::default(),
411            legacy: Vec::new(),
412            direct_families: 0,
413            direct_cells: 0,
414            direct_fragments: 0,
415            direct_complete_families: 0,
416            direct_complete_cells: 0,
417            direct_partition_families: 0,
418            direct_partition_cells: 0,
419            anchor_parses: 0,
420            anchor_asts: 0,
421            anchor_analyses: 0,
422        }
423    }
424
425    fn direct_contains(&self, row: u32, col: u32) -> bool {
426        self.direct_domains.iter().any(|(_, domain)| {
427            let rect = domain.rect();
428            row > rect.start.row
429                && row <= rect.end.row + 1
430                && col > rect.start.col
431                && col <= rect.end.col + 1
432        })
433    }
434
435    fn fallback_records(&self) -> impl Iterator<Item = &crate::engine::DeferredReplayFormula> {
436        self.replay_records.iter().filter(|record| {
437            let Some((row, col)) = record.row.checked_sub(1).zip(record.col.checked_sub(1)) else {
438                return true;
439            };
440            let coord = crate::engine::SourceCoord { row, col };
441            let disposition = match record.family {
442                Some(family) => self.disposition.shared_disposition(family, coord),
443                None => self.disposition.ordinary_disposition(coord).0,
444            };
445            !matches!(
446                disposition,
447                crate::engine::FormulaReplayCoordinateDisposition::Direct
448                    | crate::engine::FormulaReplayCoordinateDisposition::Suppressed
449            )
450        })
451    }
452}
453
454type PreparedFormulaBatches = Vec<FormulaIngestBatch>;
455type StagedFormulaBatches = Vec<(String, StagedSheet)>;
456
457type CompressedReplayBatch = (
458    FormulaIngestBatch,
459    crate::engine::FormulaCompressedSourceBatch,
460);
461type PreparedSourceBatch = (
462    FormulaIngestBatch,
463    crate::engine::FormulaCompressedSourceReport,
464    crate::engine::FormulaCompressedPreparation,
465);
466type PreparedStagedFormulaBatches = (
467    PreparedFormulaBatches,
468    Vec<CompressedReplayBatch>,
469    Vec<PreparedSourceBatch>,
470    // Formulas whose planning can fail (see `formula_may_fail_planning`).
471    FxHashSet<crate::engine::arena::AstNodeId>,
472);
473
474/// Backend-neutral source-family ingress. Adapters may prepare candidates and
475/// submit exact replay; every formula is still materialized per cell.
476#[doc(hidden)]
477pub struct SourceFormulaIngress<'a, R> {
478    engine: &'a mut Engine<R>,
479}
480
481impl<R> SourceFormulaIngress<'_, R>
482where
483    R: EvaluationContext,
484{
485    pub fn prepare_families(
486        &mut self,
487        sheet_name: &str,
488        families: &[crate::engine::SourceFormulaFamily],
489    ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
490        self.engine.ensure_not_poisoned()?;
491        self.engine.observe_function_semantic_epoch()?;
492        Ok(self
493            .engine
494            .prepare_source_formula_families(sheet_name, families))
495    }
496
497    pub fn prepare_eager_proposals(
498        &mut self,
499        sheet_name: &str,
500        families: &[crate::engine::SourceFormulaFamily],
501        partitions: &[crate::engine::PartitionedSourceFormulaFamily],
502        formula_record_count: u64,
503        replay: Box<dyn crate::engine::DeferredFormulaReplay>,
504    ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
505        self.engine.ensure_not_poisoned()?;
506        self.engine.observe_function_semantic_epoch()?;
507        let replay = Arc::new(std::sync::Mutex::new(replay));
508        self.engine.prepare_source_formula_proposals(
509            sheet_name,
510            families,
511            partitions,
512            partitions,
513            formula_record_count,
514            replay,
515            &BTreeSet::new(),
516            &Default::default(),
517            None,
518        )
519    }
520
521    ///
522    /// # Panics
523    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
524    pub fn stage_deferred(&mut self, package: crate::engine::DeferredFormulaPackage) {
525        self.engine.assert_not_poisoned();
526        self.engine.stage_deferred_formula_package(package);
527    }
528
529    pub fn ingest_replay_batches(
530        &mut self,
531        batches: Vec<(
532            FormulaIngestBatch,
533            crate::engine::FormulaCompressedSourceBatch,
534        )>,
535    ) -> Result<FormulaIngestReport, ExcelError> {
536        self.engine.ensure_not_poisoned()?;
537        self.engine
538            .ingest_compressed_formula_source_batches(batches)
539    }
540
541    pub fn finish_prepared(
542        &mut self,
543        batches: Vec<(
544            FormulaIngestBatch,
545            crate::engine::FormulaCompressedSourceReport,
546            crate::engine::FormulaCompressedPreparation,
547        )>,
548    ) -> Result<FormulaIngestReport, ExcelError> {
549        self.engine.ensure_not_poisoned()?;
550        self.engine.finish_compressed_formula_sources(batches)
551    }
552}
553
554// Computed-write coalescing pays a fixed grouping/planning cost. For very narrow
555// layers there is not enough work to amortize it, and the direct point-write path
556// is faster while preserving the same visibility semantics.
557const COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH: usize = 8;
558
559/// Equal values, numbers by bits (debug oracles).
560#[cfg(debug_assertions)]
561fn same_value_bits(a: &LiteralValue, b: &LiteralValue) -> bool {
562    match (a, b) {
563        (LiteralValue::Number(x), LiteralValue::Number(y)) => x.to_bits() == y.to_bits(),
564        _ => a == b,
565    }
566}
567
568/// Whether a layer's computed writes are buffered: not for a chain unit,
569/// whose members read the ones written before them.
570fn buffer_layer_writes(layer: &crate::engine::scheduler::Layer) -> bool {
571    !layer.sequential && layer.vertices.len() >= COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH
572}
573/// Adaptive layer parallelism (see `evaluate_layer_parallel`): a layer runs
574/// sequentially until either it has run `PARALLEL_LAYER_PROBE` or the rest,
575/// at the rate so far, is estimated at `PARALLEL_LAYER_WORTH` or more; then
576/// the rest goes to the thread pool. Tuned on Enron first eval (probe 30 /
577/// 100 / 300 µs, worth 50 / 150 / 400 µs; 300 / 150 best, no workbook
578/// slower).
579/// Candidate count from which schedule preparation sorts on the pool.
580const PARALLEL_SCHEDULE_MIN_CANDIDATES: usize = 16 * 1024;
581/// Plan reuse: a schedule of more candidates than this can become the
582/// base, and a request takes the base restricted to it when the base is at
583/// most `BASE_SCHEDULE_RATIO` times the request.
584const BASE_SCHEDULE_MIN_VERTICES: usize = 64;
585const BASE_SCHEDULE_RATIO: usize = 256;
586/// A restricted schedule keeps the base's layers, which can split a family
587/// run the planner would keep whole (each piece then builds its own
588/// criteria index): below this many candidates planning costs less than
589/// that (real_ops_model: 75 candidates, 31 us to plan, +0.3 ms restricted).
590const BASE_SCHEDULE_MIN_REQUEST: usize = 128;
591const PARALLEL_LAYER_PROBE: std::time::Duration = std::time::Duration::from_micros(300);
592const PARALLEL_LAYER_WORTH: std::time::Duration = std::time::Duration::from_micros(150);
593/// A member this expensive (ns, measured by the probe) is its own task.
594const EXPENSIVE_VERTEX_NS: u128 = 10_000;
595/// [`Engine::live_cancellation_after_work`] messages.
596const UNIT_CANCELLED: &str = "Evaluation cancelled; the evaluated unit was not committed";
597const GROUP_CANCELLED: &str =
598    "Parallel evaluation cancelled; the evaluated group was not committed";
599const SCC_CANCELLED: &str = "Evaluation cancelled; the evaluated cycle was not completed";
600
601#[derive(Debug, Clone, PartialEq)]
602pub(crate) enum ComputedWrite {
603    Cell {
604        seq: u64,
605        sheet_id: SheetId,
606        row0: u32,
607        col0: u32,
608        value: OverlayValue,
609        format_id: Option<crate::format::FormatId>,
610    },
611    Rect {
612        seq: u64,
613        sheet_id: SheetId,
614        sr0: u32,
615        sc0: u32,
616        values: Vec<Vec<OverlayValue>>,
617    },
618    /// Consecutive rows `row0..` of one column (a family run's commit),
619    /// each with its format.
620    Run {
621        seq: u64,
622        sheet_id: SheetId,
623        row0: u32,
624        col0: u32,
625        entries: Vec<(OverlayValue, Option<crate::format::FormatId>)>,
626    },
627}
628
629impl ComputedWrite {
630    #[inline]
631    pub(crate) fn seq(&self) -> u64 {
632        match self {
633            ComputedWrite::Cell { seq, .. }
634            | ComputedWrite::Rect { seq, .. }
635            | ComputedWrite::Run { seq, .. } => *seq,
636        }
637    }
638}
639
640#[derive(Debug)]
641struct BufferedReadClaim {
642    seq: u64,
643    result: Arc<crate::engine::effects::RecordedResult>,
644    winning: bool,
645}
646
647#[derive(Debug, Clone, Copy)]
648enum OverlayWriteDisposition {
649    Installed,
650    Buffered,
651    Skipped,
652}
653
654#[derive(Debug, Default)]
655pub(crate) struct ComputedWriteBuffer {
656    writes: Vec<ComputedWrite>,
657    next_seq: u64,
658    estimated_bytes: usize,
659    formats_present: bool,
660    // Only dynamic results allocate entries. Existing coalescer sequences
661    // identify the final installed write, including Cell/Rect/Run overwrites.
662    read_claims: FxHashMap<(SheetId, u32, u32), BufferedReadClaim>,
663}
664
665impl ComputedWriteBuffer {
666    const ENTRY_BASE_BYTES: usize = 32;
667
668    #[inline]
669    pub(crate) fn is_empty(&self) -> bool {
670        self.writes.is_empty()
671    }
672
673    #[inline]
674    pub(crate) fn len(&self) -> usize {
675        self.writes.len()
676    }
677
678    #[inline]
679    pub(crate) fn writes(&self) -> &[ComputedWrite] {
680        &self.writes
681    }
682
683    #[inline]
684    pub(crate) fn estimated_bytes(&self) -> usize {
685        self.estimated_bytes
686    }
687
688    #[cfg(feature = "tracing")]
689    fn traced_cell_count(&self) -> usize {
690        self.writes
691            .iter()
692            .map(|write| match write {
693                ComputedWrite::Cell { .. } => 1,
694                ComputedWrite::Rect { values, .. } => values.iter().map(Vec::len).sum(),
695                ComputedWrite::Run { entries, .. } => entries.len(),
696            })
697            .sum()
698    }
699
700    pub(crate) fn push_cell(
701        &mut self,
702        sheet_id: SheetId,
703        row0: u32,
704        col0: u32,
705        value: OverlayValue,
706    ) {
707        self.push_cell_with_format(sheet_id, row0, col0, value, None);
708    }
709
710    pub(crate) fn push_cell_with_format(
711        &mut self,
712        sheet_id: SheetId,
713        row0: u32,
714        col0: u32,
715        value: OverlayValue,
716        format_id: Option<crate::format::FormatId>,
717    ) {
718        let format_id = format_id.filter(|id| *id != crate::format::FormatId::GENERAL);
719        self.formats_present |= format_id.is_some();
720        let seq = self.next_sequence();
721        self.estimated_bytes = self
722            .estimated_bytes
723            .saturating_add(Self::estimate_value_bytes(&value));
724        self.writes.push(ComputedWrite::Cell {
725            seq,
726            sheet_id,
727            row0,
728            col0,
729            value,
730            format_id,
731        });
732    }
733
734    pub(crate) fn push_column_run(
735        &mut self,
736        sheet_id: SheetId,
737        row0: u32,
738        col0: u32,
739        mut entries: Vec<(OverlayValue, Option<crate::format::FormatId>)>,
740    ) {
741        let seq = self.next_sequence();
742        let mut added = 0usize;
743        for (value, format_id) in entries.iter_mut() {
744            *format_id = format_id.filter(|id| *id != crate::format::FormatId::GENERAL);
745            self.formats_present |= format_id.is_some();
746            added = added.saturating_add(Self::estimate_value_bytes(value));
747        }
748        self.estimated_bytes = self.estimated_bytes.saturating_add(added);
749        self.writes.push(ComputedWrite::Run {
750            seq,
751            sheet_id,
752            row0,
753            col0,
754            entries,
755        });
756    }
757
758    pub(crate) fn push_rect(
759        &mut self,
760        sheet_id: SheetId,
761        sr0: u32,
762        sc0: u32,
763        values: Vec<Vec<OverlayValue>>,
764    ) {
765        let seq = self.next_sequence();
766        let added = values
767            .iter()
768            .flat_map(|row| row.iter())
769            .map(Self::estimate_value_bytes)
770            .fold(0usize, usize::saturating_add);
771        self.estimated_bytes = self.estimated_bytes.saturating_add(added);
772        self.writes.push(ComputedWrite::Rect {
773            seq,
774            sheet_id,
775            sr0,
776            sc0,
777            values,
778        });
779    }
780
781    pub(crate) fn clear(&mut self) {
782        self.writes.clear();
783        self.read_claims.clear();
784        self.estimated_bytes = 0;
785        self.formats_present = false;
786    }
787
788    fn take_writes(&mut self) -> (Vec<ComputedWrite>, bool) {
789        self.estimated_bytes = 0;
790        let formats_present = std::mem::take(&mut self.formats_present);
791        (std::mem::take(&mut self.writes), formats_present)
792    }
793
794    fn next_sequence(&mut self) -> u64 {
795        let seq = self.next_seq;
796        self.next_seq = self.next_seq.wrapping_add(1);
797        seq
798    }
799
800    #[inline]
801    fn estimate_value_bytes(value: &OverlayValue) -> usize {
802        Self::ENTRY_BASE_BYTES.saturating_add(value.estimated_payload_bytes())
803    }
804}
805
806#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
807struct ComputedWriteChunkKey {
808    sheet_id: SheetId,
809    col0: u32,
810    chunk_idx: usize,
811    chunk_start_row0: u32,
812}
813
814#[derive(Debug, Clone, PartialEq)]
815pub(crate) struct ComputedWriteChunkEntryPlan {
816    pub(crate) row_in_chunk: usize,
817    pub(crate) seq: u64,
818    pub(crate) value: OverlayValue,
819    pub(crate) format_id: Option<crate::format::FormatId>,
820}
821
822#[derive(Debug, Clone, PartialEq, Eq)]
823pub(crate) enum ComputedWriteChunkPlanShape {
824    Point,
825    SparseOffsets {
826        entries: usize,
827        span_len: usize,
828    },
829    DenseRange {
830        start: usize,
831        len: usize,
832    },
833    RunRange {
834        start: usize,
835        len: usize,
836        runs: usize,
837    },
838}
839
840#[derive(Debug, Clone, PartialEq, Eq)]
841pub(crate) enum ComputedWriteFormatClear {
842    Range { start: usize, end: usize },
843    Offsets(Vec<usize>),
844}
845
846#[derive(Debug, Clone, PartialEq)]
847pub(crate) enum ComputedWriteChunkFormatEffect {
848    NoFormatWork,
849    ClearStale(ComputedWriteFormatClear),
850    SetExplicit(Vec<(usize, Option<crate::format::FormatId>)>),
851}
852
853#[derive(Debug, Clone, PartialEq)]
854pub(crate) struct ComputedWriteChunkPlan {
855    pub(crate) sheet_id: SheetId,
856    pub(crate) col0: u32,
857    pub(crate) chunk_idx: usize,
858    pub(crate) chunk_start_row0: u32,
859    pub(crate) entries: Vec<ComputedWriteChunkEntryPlan>,
860    pub(crate) shape: ComputedWriteChunkPlanShape,
861    pub(crate) format_effect: ComputedWriteChunkFormatEffect,
862}
863
864#[derive(Debug, Clone, Default, PartialEq)]
865pub(crate) struct ComputedWriteCoalescingPlan {
866    pub(crate) chunks: Vec<ComputedWriteChunkPlan>,
867    pub(crate) input_cells: usize,
868    pub(crate) coalesced_cells: usize,
869    pub(crate) overwritten_cells: usize,
870}
871
872impl ComputedWriteCoalescingPlan {
873    #[inline]
874    pub(crate) fn is_empty(&self) -> bool {
875        self.chunks.is_empty()
876    }
877}
878
879impl ComputedWriteChunkPlan {
880    fn from_group(
881        key: ComputedWriteChunkKey,
882        mut entries: Vec<ComputedWriteChunkEntryPlan>,
883        formats_present: bool,
884        computed_lane_has_formats: bool,
885    ) -> (Self, usize) {
886        entries.sort_by_key(|entry| (entry.row_in_chunk, entry.seq));
887        let input_len = entries.len();
888        let mut coalesced: Vec<ComputedWriteChunkEntryPlan> = Vec::with_capacity(input_len);
889        for entry in entries {
890            if let Some(prev) = coalesced.last_mut()
891                && prev.row_in_chunk == entry.row_in_chunk
892            {
893                *prev = entry;
894                continue;
895            }
896            coalesced.push(entry);
897        }
898        let overwritten = input_len.saturating_sub(coalesced.len());
899        let shape = Self::classify_shape(&coalesced);
900        let format_effect = Self::classify_format_effect(
901            &coalesced,
902            &shape,
903            formats_present,
904            computed_lane_has_formats,
905        );
906        (
907            Self {
908                sheet_id: key.sheet_id,
909                col0: key.col0,
910                chunk_idx: key.chunk_idx,
911                chunk_start_row0: key.chunk_start_row0,
912                entries: coalesced,
913                shape,
914                format_effect,
915            },
916            overwritten,
917        )
918    }
919
920    fn classify_format_effect(
921        entries: &[ComputedWriteChunkEntryPlan],
922        shape: &ComputedWriteChunkPlanShape,
923        formats_present: bool,
924        computed_lane_has_formats: bool,
925    ) -> ComputedWriteChunkFormatEffect {
926        if !formats_present {
927            return if computed_lane_has_formats {
928                ComputedWriteChunkFormatEffect::ClearStale(Self::format_clear_spec(entries, shape))
929            } else {
930                ComputedWriteChunkFormatEffect::NoFormatWork
931            };
932        }
933
934        if entries.iter().all(|entry| entry.format_id.is_none()) {
935            return if computed_lane_has_formats {
936                ComputedWriteChunkFormatEffect::ClearStale(Self::format_clear_spec(entries, shape))
937            } else {
938                ComputedWriteChunkFormatEffect::NoFormatWork
939            };
940        }
941
942        ComputedWriteChunkFormatEffect::SetExplicit(
943            entries
944                .iter()
945                .map(|entry| (entry.row_in_chunk, entry.format_id))
946                .collect(),
947        )
948    }
949
950    fn format_clear_spec(
951        entries: &[ComputedWriteChunkEntryPlan],
952        shape: &ComputedWriteChunkPlanShape,
953    ) -> ComputedWriteFormatClear {
954        match shape {
955            ComputedWriteChunkPlanShape::DenseRange { start, len }
956            | ComputedWriteChunkPlanShape::RunRange { start, len, .. } => {
957                ComputedWriteFormatClear::Range {
958                    start: *start,
959                    end: start.saturating_add(*len),
960                }
961            }
962            ComputedWriteChunkPlanShape::Point
963            | ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
964                ComputedWriteFormatClear::Offsets(
965                    entries.iter().map(|entry| entry.row_in_chunk).collect(),
966                )
967            }
968        }
969    }
970
971    fn classify_shape(entries: &[ComputedWriteChunkEntryPlan]) -> ComputedWriteChunkPlanShape {
972        debug_assert!(!entries.is_empty());
973        if entries.len() == 1 {
974            return ComputedWriteChunkPlanShape::Point;
975        }
976
977        let start = entries[0].row_in_chunk;
978        let end = entries[entries.len() - 1].row_in_chunk;
979        let span_len = end.saturating_sub(start).saturating_add(1);
980        if span_len != entries.len() {
981            return ComputedWriteChunkPlanShape::SparseOffsets {
982                entries: entries.len(),
983                span_len,
984            };
985        }
986
987        let runs = Self::run_count(entries);
988        if runs < entries.len() {
989            ComputedWriteChunkPlanShape::RunRange {
990                start,
991                len: entries.len(),
992                runs,
993            }
994        } else {
995            ComputedWriteChunkPlanShape::DenseRange {
996                start,
997                len: entries.len(),
998            }
999        }
1000    }
1001
1002    fn run_count(entries: &[ComputedWriteChunkEntryPlan]) -> usize {
1003        let mut runs = 0usize;
1004        let mut prev: Option<&OverlayValue> = None;
1005        for entry in entries {
1006            if prev != Some(&entry.value) {
1007                runs = runs.saturating_add(1);
1008                prev = Some(&entry.value);
1009            }
1010        }
1011        runs
1012    }
1013}
1014
1015#[cfg(feature = "tracing")]
1016#[derive(Default)]
1017struct TraceEvaluationCounters {
1018    computed_vertices: usize,
1019    cycles: usize,
1020}
1021
1022use crate::traits::{NamedRangeResolver as _, SourceResolver as _};
1023
1024pub struct Engine<R> {
1025    poisoned: Arc<std::sync::atomic::AtomicBool>,
1026    #[cfg(test)]
1027    pub(crate) panic_internal_once: bool,
1028    #[cfg(test)]
1029    pub(crate) panic_kernel_after_member: std::sync::atomic::AtomicUsize,
1030    criteria_mask_cache: criteria_mask_cache::CriteriaMaskCache,
1031    pub(crate) graph: DependencyGraph,
1032    resolver: crate::guarded_resolver::GuardedResolver<R>,
1033    pub(super) resolved_functions: crate::interpreter::ResolvedFunctions,
1034    pub(super) operation_diagnostic_collector:
1035        Option<Arc<crate::operation_diagnostics::OperationDiagnosticCollector>>,
1036    resolved_sheet_names:
1037        FxHashMap<crate::engine::arena::StringId, (crate::SheetId, Option<usize>)>,
1038    resolution_sheet_revision: Option<(u64, u64, usize)>,
1039    resolution_arrow_sheet_names: Vec<Arc<str>>,
1040    resolved_arrow_sheets: Vec<Option<usize>>,
1041    /// Stable-ID index, invalidated before exposing arbitrary mutable storage.
1042    arrow_sheet_index: std::sync::OnceLock<Vec<Option<usize>>>,
1043    pub config: EvalConfig,
1044    workbook_load_limits: crate::engine::WorkbookLoadLimits,
1045    /// Clock for volatile date/time builtins, wrapped in a per-recalc
1046    /// snapshot: sampled once at the start of every evaluation request
1047    /// ([`Self::begin_evaluation_request`]) so all `NOW()`/`TODAY()` reads in
1048    /// one recalc — including SCC iteration passes — agree (spec §7.11).
1049    clock: crate::timezone::SnapshotClock,
1050    thread_pool: Option<Arc<rayon::ThreadPool>>,
1051    pub recalc_epoch: u64,
1052    resumable: Option<super::resumable::LogicalRecalculation>,
1053    resumable_call: bool,
1054    recalc_options: Option<super::RecalcJobOptions>,
1055    #[cfg(test)]
1056    pub(crate) resumable_stop_after_units_for_test: Option<usize>,
1057    snapshot_id: std::sync::atomic::AtomicU64,
1058    // Admission-only signal: source publication must not change ordinary
1059    // snapshots or invalidate unrelated lookup/planning/bounds caches.
1060    source_mutation_generation: u64,
1061    topology_epoch: u64,
1062    cached_static_schedule: Option<CachedScheduleEntry>,
1063    /// Program 3 (plan reuse): schedules of recent earlier requests, most
1064    /// recent last (a user alternating between a few inputs recalculates
1065    /// the same few closures). Bounded by `RECENT_SCHEDULES` entries and
1066    /// `RECENT_SCHEDULE_VERTICES` candidate vertices in total.
1067    recent_schedules: Vec<CachedScheduleEntry>,
1068    /// Program 3 (plan reuse): the largest current schedule seen (usually
1069    /// the first evaluation's). A request it covers, and that is not much
1070    /// smaller, takes its schedule restricted to the request instead of
1071    /// planning (`Schedule::restrict`).
1072    base_schedule: Option<CachedScheduleEntry>,
1073    #[cfg(any(test, feature = "benchmark_internal"))]
1074    recalc_reuse_probe: std::sync::Mutex<RecalcReuseProbe>,
1075    spill_mgr: ShimSpillManager,
1076    /// Arrow-backed storage for sheet values (Phase A)
1077    arrow_sheets: SheetStore,
1078    /// Workbook-local number-format registry.
1079    format_registry: crate::format::FormatRegistry,
1080    /// Derived formula formats keyed by grid position, never graph vertex identity.
1081    derived_formats: crate::engine::derived_formats::DerivedFormats,
1082    #[cfg(test)]
1083    derived_format_operations_for_test: std::sync::atomic::AtomicU64,
1084    #[cfg(test)]
1085    family_members_for_test: std::sync::atomic::AtomicU64,
1086    #[cfg(test)]
1087    invariant_bound_members_for_test: std::sync::atomic::AtomicU64,
1088    #[cfg(test)]
1089    lifted_members_for_test: std::sync::atomic::AtomicU64,
1090    #[cfg(test)]
1091    chained_members_for_test: std::sync::atomic::AtomicU64,
1092    #[cfg(test)]
1093    lane_clean_reads_for_test: std::sync::atomic::AtomicU64,
1094    #[cfg(test)]
1095    criteria_kernel_members_for_test: std::sync::atomic::AtomicU64,
1096    #[cfg(test)]
1097    memo_hits_for_test: std::sync::atomic::AtomicU64,
1098    /// Authority build last compressed (`maybe_compress_formulas`).
1099    compressed_at_build: Option<u64>,
1100    #[cfg(test)]
1101    computed_overlay_set_explicit_entry_operations_for_test: u64,
1102    #[cfg(test)]
1103    computed_overlay_stale_clear_range_effects_for_test: u64,
1104    #[cfg(test)]
1105    computed_overlay_stale_clear_offset_attempts_for_test: u64,
1106    #[cfg(test)]
1107    computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64,
1108    /// True if any edit after bulk load; disables Arrow reads for parity
1109    has_edited: bool,
1110    /// Overlay compaction counter (Phase C instrumentation)
1111    overlay_compactions: u64,
1112
1113    // Overlay memory observability / budget (ticket 503)
1114    computed_overlay_bytes_estimate: usize,
1115    computed_overlay_mirroring_disabled: bool,
1116    /// When true, RangeView resolution materializes from graph/Arrow base per-cell.
1117    /// This preserves correctness if we stop mirroring formula/spill outputs into computed overlays.
1118    pub(crate) force_materialize_range_views: bool,
1119    // Pass-scoped cache for Arrow used-row bounds per column
1120    row_bounds_cache: std::sync::RwLock<Option<RowBoundsCache>>,
1121    // Snapshot-scoped final used-axis bounds for open-ended references.
1122    used_axis_bounds_cache: std::sync::RwLock<Option<UsedAxisBoundsCache>>,
1123    lookup_index_cache: LookupIndexCache,
1124    source_cache: Arc<std::sync::RwLock<SourceCache>>,
1125    /// Identity binding for opaque source-family preparations.
1126    source_formula_token: Arc<()>,
1127    /// Load-time staging of relocated formula copies (see `SourceFamilyMode`).
1128    pub(crate) source_family_mode: crate::engine::SourceFamilyMode,
1129    pub(crate) source_family_counters: crate::engine::SourceFamilyCounters,
1130    /// Dedicated identity binding for reusable recalculation plans.
1131    recalc_plan_token: Arc<()>,
1132    /// Staged formulas by sheet when `defer_graph_building` is enabled.
1133    staged_formulas: StagedFormulaMap,
1134    /// Presence and generation authority for ordinary staged formula discovery.
1135    staged_formula_index: StagedFormulaIndex,
1136    // Occupancy invalidation only: never a formula/read dependency.
1137    blocked_pending_spills: Vec<(VertexId, CellRef, Region)>,
1138    /// Anchors whose spill is blocked by a formula or by another spill's
1139    /// cell, keyed by that blocking cell: a change to the cell (an edit, or
1140    /// the other spill clearing) wakes them. Never a read dependency.
1141    spill_blocker_waiters: SpillBlockerWaiters,
1142    /// Per-sheet row visibility sidecar state.
1143    row_visibility: FxHashMap<SheetId, RowVisibilityState>,
1144    /// Cached row visibility masks keyed by sheet/span/mode/version.
1145    row_visibility_mask_cache: std::sync::RwLock<
1146        FxHashMap<VisibilityMaskCacheKey, std::sync::Arc<arrow_array::BooleanArray>>,
1147    >,
1148    /// Non-fatal malformed formula diagnostics captured during ingest/graph-build.
1149    formula_parse_diagnostics: Vec<FormulaParseDiagnostic>,
1150    /// Last centralized formula ingest report.
1151    last_formula_ingest_report: Option<FormulaIngestReport>,
1152    /// Aggregate centralized formula ingest report for this engine.
1153    formula_ingest_report_total: FormulaIngestReport,
1154    /// Transient cancellation flag used during evaluation
1155    active_cancel_flag: Option<crate::engine::CancelToken>,
1156    /// Transient absolute deadline used by composed target and plan requests.
1157    active_evaluation_deadline: Option<Instant>,
1158
1159    /// Engine-level action depth.
1160    ///
1161    /// Ticket 614 introduces `Engine::action` as a stable, commit-only transaction surface.
1162    /// Nested actions are currently disallowed (deterministic rule) and will return an error.
1163    action_depth: u32,
1164
1165    // Phase 3b virtual-dependency convergence telemetry
1166    last_virtual_dep_telemetry: VirtualDepTelemetry,
1167    virtual_dep_fallback_activations: u64,
1168
1169    // Runtime-cycle SCC evaluation telemetry (RFC #112, Stage 2)
1170    last_cycle_telemetry: CycleTelemetry,
1171
1172    // C0 evaluation-resource observability. IDs are never reset or reused.
1173    next_evaluation_resource_request_id: u64,
1174    evaluation_resource_request_depth: usize,
1175    active_evaluation_resource_request: Option<EvaluationResourceRequestStats>,
1176    last_evaluation_resource_request: Option<EvaluationResourceRequestStats>,
1177    evaluation_resource_baseline: EvaluationResourceBaselineStats,
1178    evaluation_resource_request_started_at: Option<crate::instant::FzInstant>,
1179    evaluation_resource_budgets: crate::engine::EvaluationBudgets,
1180    evaluation_resource_config_diagnostic:
1181        Option<crate::engine::EvaluationResourceConfigDiagnostic>,
1182    active_resource_ledger: Option<ResourceLedger>,
1183    source_cache_footprints: Vec<std::sync::Weak<std::sync::atomic::AtomicU64>>,
1184    source_cache_accounted: u64,
1185
1186    /// SCC members that entered iterative calculation (`CyclePolicy::Iterate`
1187    /// with a witnessed live cycle) during the current evaluation request
1188    /// and must re-run on the next one.
1189    ///
1190    /// Excel re-evaluates circular cells on EVERY recalc (the accumulator
1191    /// contract, spec §4/§7.6), but this engine's dirty model marks SCC
1192    /// members clean after a recalc and would otherwise skip them forever.
1193    /// Resolution: members of iterating SCCs are redirtied volatile-like at
1194    /// the end of the same recalc that iterated them
1195    /// ([`Self::redirty_for_next_recalc`], called wherever
1196    /// `redirty_volatiles` runs). The set is per-recalc, never persisted:
1197    /// if an edit breaks the cycle, the next recalc's SCC task either does
1198    /// not exist or settles as phantom, nothing re-registers, and the
1199    /// redirty chain stops by itself.
1200    ///
1201    /// SCCs that landed on an exact fixed point are exempt: they go to
1202    /// [`Self::retained_scc_members`] instead and stay clean until the dirty
1203    /// graph (or a config change) reaches them (#368).
1204    pending_iterative_redirty: Vec<VertexId>,
1205    /// Opt-in snapshot request: defer only next-cycle volatile redirty.
1206    snapshot_evaluation_active: bool,
1207    snapshot_volatile_redirty_pending: bool,
1208    /// Completed snapshot SCCs stay Current until the next request chooses work.
1209    snapshot_iterative_redirty_pending: Vec<VertexId>,
1210    /// Members of iterating SCCs retained across recalcs (#368), keyed to the
1211    /// id of the retained SCC they belong to (ids come from
1212    /// `next_retained_scc_id`; grouping is only used for telemetry).
1213    ///
1214    /// An SCC is retained when the recalc that iterated it stopped because
1215    /// every member reproduced its previous value exactly (|Δ| = 0, or
1216    /// identity for non-numeric members; never NaN-converged), before the
1217    /// `max_iterations` cap, with no volatile or dynamic-reference member.
1218    /// Such an SCC is a fixed point of its own inputs: running it again with
1219    /// the same inputs cannot change any value, so it is not redirtied. The
1220    /// dirty graph remains the validity authority — any edit that reaches a
1221    /// member dirties it like any other formula and the SCC task re-runs.
1222    /// Membership is dropped when a member runs in an SCC task again (it is
1223    /// then re-retained or re-registered for per-recalc redirty), when the
1224    /// vertex is deleted, or when
1225    /// [`Self::reconcile_retained_sccs_at_request_begin`] invalidates the
1226    /// whole set because a config knob outside the graph changed.
1227    retained_scc_members: FxHashMap<VertexId, u64>,
1228    next_retained_scc_id: u64,
1229    /// [`Self::retained_scc_config_fingerprint`] as of the last retention.
1230    /// `Engine::config` is a public field, so knobs that change a retained
1231    /// SCC's result (cycle policy/tolerance, date system, determinism,
1232    /// volatile seeding) can change between recalcs without touching the
1233    /// graph; a mismatch at request begin dirties every retained member.
1234    /// Only meaningful while `retained_scc_members` is non-empty.
1235    retained_scc_config_fingerprint: u64,
1236    /// Function-registry semantic epoch and runtime-provider revision as of
1237    /// the last time retained SCCs were reconciled against them. A newer
1238    /// epoch dirties only the retained members whose formula calls a changed
1239    /// function (or every member when the change log is incomplete).
1240    retained_scc_function_epoch_seen: u64,
1241    retained_scc_provider_revision_seen: Option<u64>,
1242    /// Retained members that were already dirty when the current request
1243    /// began, with the SCC id they carried. A member still holding that id
1244    /// at the end of the request was not touched by any SCC task — its
1245    /// cycle dissolved (it evaluated as an ordinary formula) or the request
1246    /// never reached it — so [`Self::redirty_for_next_recalc`] drops it from
1247    /// the retained set instead of letting it linger.
1248    retained_scc_dirty_at_begin: Vec<(VertexId, u64)>,
1249
1250    /// Final committed values of iterating-SCC members (spec §4 persistence).
1251    /// In canonical (value-cache disabled) mode the computed overlay is the
1252    /// ONLY home of a formula's value, and structural edits clear computed
1253    /// overlays wholesale (`clear_computed_overlay_after_row/_col`) —
1254    /// destroying iteration state (accumulators reset to 0; found by the
1255    /// iterate edge corpus). This snapshot lets the next SCC task re-seed
1256    /// members whose overlay entry vanished. Members registered for
1257    /// per-recalc redirty are refreshed by
1258    /// [`Self::redirty_for_next_recalc`]; retained members are written once
1259    /// when retained. Entries are dropped when the member's SCC task ends
1260    /// without iterating or when the vertex is deleted. Empty unless
1261    /// something iterated — zero cost otherwise.
1262    iterative_state_values: FxHashMap<VertexId, LiteralValue>,
1263
1264    /// Global function-registry semantic epoch last observed.
1265    function_semantic_epoch_seen: u64,
1266    /// Runtime-provider semantic revision last observed.
1267    function_provider_revision_seen: Option<u64>,
1268
1269    #[cfg(feature = "tracing")]
1270    trace_evaluation_counters: TraceEvaluationCounters,
1271    #[cfg(test)]
1272    evaluation_request_begin_count_for_test: u64,
1273    #[cfg(any(test, feature = "test-support"))]
1274    before_prepared_span_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1275    #[cfg(test)]
1276    before_target_preparation_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1277    #[cfg(test)]
1278    before_target_planning_snapshot_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1279    #[cfg(test)]
1280    inject_target_semantic_stale_once_for_test: bool,
1281    #[cfg(test)]
1282    force_virtual_dep_changes_remaining_for_test: usize,
1283    /// Dynamic-read freshness state (design §8.2).
1284    freshness: freshness::Freshness,
1285    #[cfg(test)]
1286    fail_evaluation_commit_preflight_once_for_test: bool,
1287    #[cfg(test)]
1288    target_preparation_fault_for_test:
1289        Option<crate::engine::target_preparation::TargetPreparationFault>,
1290    #[cfg(test)]
1291    force_non_cycle_schedule_fallback_for_test: bool,
1292    #[cfg(test)]
1293    before_legacy_fallback_final_provider_sample_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1294    #[cfg(test)]
1295    after_eager_proposal_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1296}
1297
1298/// This wrapper is intentionally thin for ticket 614 (commit-only): it delegates to existing
1299/// `Engine` edit methods and does not create changelog boundaries or implement rollback.
1300impl<R: EvaluationContext> Engine<R> {
1301    pub(crate) fn ingest_pipeline(&mut self) -> crate::engine::ingest_pipeline::IngestPipeline<'_> {
1302        self.graph.ingest_pipeline(&self.resolver)
1303    }
1304}
1305
1306pub struct EngineAction<'a, R>
1307where
1308    R: EvaluationContext,
1309{
1310    engine: &'a mut Engine<R>,
1311    name: String,
1312    // Complete private mutation capture used by atomic actions.
1313    // Stored as a raw pointer to avoid creating aliasing `&mut` borrows alongside `&mut Engine`.
1314    capture: Option<*mut MutationCapture>,
1315    // Optional Arrow undo journal used by `Engine::action_atomic`.
1316    // Stored as a raw pointer to avoid aliasing issues with `&mut Engine`.
1317    arrow_undo: Option<*mut crate::engine::ArrowUndoBatch>,
1318    // True when this EngineAction must enforce conservative atomic transaction policy.
1319    atomic_policy: bool,
1320    // Incremental action-local ownership history, kept in the current frame.
1321    // The flag retains mask eligibility for untouched projections and explicit
1322    // clears. Literal/formula writes revoke it; no-spill edits add nothing.
1323    // Keep released entries to distinguish later user writes from projections.
1324    spill_projection_history:
1325        std::collections::HashMap<CellRef, bool, formualizer_common::CoordBuildHasher>,
1326}
1327
1328impl<'a, R> EngineAction<'a, R>
1329where
1330    R: EvaluationContext,
1331{
1332    fn remember_spill_clears(&mut self, events: &[ChangeEvent]) {
1333        for event in events {
1334            if let ChangeEvent::SpillCleared { old, .. } = event {
1335                for &cell in &old.target_cells {
1336                    self.spill_projection_history.entry(cell).or_insert(true);
1337                }
1338            }
1339        }
1340    }
1341
1342    /// Capture former projections in the old frame before structural invalidation.
1343    /// Compaction can erase both computed and delta entries, so mask eligibility
1344    /// follows action-local user writes rather than overlay presence.
1345    fn spill_history_masks_before_insert(
1346        &self,
1347        sheet: SheetId,
1348        before: u32,
1349        rows: bool,
1350    ) -> Vec<CellRef> {
1351        let name = self.engine.graph.sheet_name(sheet);
1352        self.spill_projection_history
1353            .iter()
1354            .filter_map(|(&cell, &mask)| {
1355                let axis = if rows {
1356                    cell.coord.row()
1357                } else {
1358                    cell.coord.col()
1359                };
1360                (mask
1361                    && cell.sheet_id == sheet
1362                    && axis >= before
1363                    && self
1364                        .engine
1365                        .read_cell_formula_ast(name, cell.coord.row() + 1, cell.coord.col() + 1)
1366                        .is_none())
1367                .then_some(cell)
1368            })
1369            .collect()
1370    }
1371
1372    fn shift_spill_history(&mut self, sheet: SheetId, before: u32, count: u32, rows: bool) {
1373        self.spill_projection_history = self
1374            .spill_projection_history
1375            .drain()
1376            .map(|(cell, mask)| {
1377                if cell.sheet_id != sheet {
1378                    return (cell, mask);
1379                }
1380                let (mut row, mut col) = (cell.coord.row(), cell.coord.col());
1381                if rows && row >= before {
1382                    row = row.saturating_add(count);
1383                }
1384                if !rows && col >= before {
1385                    col = col.saturating_add(count);
1386                }
1387                (CellRef::new(sheet, Coord::new(row, col, true, true)), mask)
1388            })
1389            .collect();
1390    }
1391
1392    #[inline]
1393    fn addr_for(&mut self, sheet: &str, row: u32, col: u32) -> crate::reference::CellRef {
1394        let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1395        let coord = crate::reference::Coord::from_excel(row, col, true, true);
1396        crate::reference::CellRef::new(sheet_id, coord)
1397    }
1398
1399    #[inline]
1400    pub fn name(&self) -> &str {
1401        &self.name
1402    }
1403
1404    #[inline]
1405    pub fn set_cell_value(
1406        &mut self,
1407        sheet: &str,
1408        row: u32,
1409        col: u32,
1410        value: LiteralValue,
1411    ) -> Result<(), crate::engine::EditorError> {
1412        self.engine.ensure_not_poisoned()?;
1413        if self.capture.is_some() {
1414            let old_value = self.engine.read_cell_value(sheet, row, col);
1415            let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1416            let addr = self.addr_for(sheet, row, col);
1417            let Some(capture_ptr) = self.capture else {
1418                return Err(crate::engine::EditorError::TransactionFailed {
1419                    reason: "action_with_logger: missing mutation capture".to_string(),
1420                });
1421            };
1422
1423            // For atomic journal mode, record computed overlay effects for this cell.
1424            // Delta-overlay undo is recorded semantically based on old_value/old_formula.
1425            let old_comp = if self.arrow_undo.is_some() {
1426                self.engine.read_computed_overlay_cell(sheet, row, col)
1427            } else {
1428                None
1429            };
1430
1431            if self.engine.graph_admission_enabled() {
1432                let admission =
1433                    self.engine
1434                        .graph
1435                        .preview_value_mutation(addr.sheet_id, row, col)?;
1436                self.engine.preflight_graph_admission(admission)?;
1437            }
1438            if old_formula.is_none() {
1439                old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1440            }
1441
1442            let old_was_spill_child = self
1443                .engine
1444                .graph
1445                .spill_registry_anchor_for_cell(addr)
1446                .is_some_and(|owner| self.engine.graph.get_cell_ref(owner) != Some(addr));
1447            let delta_old_sem = if old_formula.is_some()
1448                || old_was_spill_child
1449                || self.spill_projection_history.get(&addr) == Some(&true)
1450            {
1451                // An untouched former projection is restored by the spill snapshot.
1452                // Once a user write revokes mask eligibility, old_value is the
1453                // current literal instead: keep it so redo can remove/replace it.
1454                None
1455            } else {
1456                Some(old_value.clone().unwrap_or(LiteralValue::Empty))
1457            };
1458
1459            let start_len = unsafe { (&*capture_ptr).len() };
1460
1461            // Safety: `capture_ptr` comes from a unique operation-local `&mut MutationCapture`.
1462            let capture = unsafe { &mut *capture_ptr };
1463            self.engine.edit_with_capture(capture, |editor| {
1464                editor.set_cell_value_with_old_state(
1465                    addr,
1466                    value.clone(),
1467                    old_value.clone(),
1468                    old_formula.clone(),
1469                );
1470            })?;
1471            self.engine.record_structural_change(StructuralScope::Cell {
1472                sheet: addr.sheet_id,
1473                row: addr.coord.row(),
1474                col: addr.coord.col(),
1475            });
1476
1477            self.remember_spill_clears(&unsafe { (&*capture_ptr).events() }[start_len..]);
1478            let clears_projection = matches!(value, LiteralValue::Empty)
1479                && self.spill_projection_history.contains_key(&addr);
1480            if clears_projection {
1481                // Empty releases an earlier literal so a later spill can fill
1482                // this cell. Keep a computed tombstone to hide compacted base
1483                // values until then, rather than a delta that masks new output.
1484                self.engine.clear_delta_overlay_cell(sheet, row, col);
1485                self.engine.set_computed_overlay_cell_raw(
1486                    sheet,
1487                    row,
1488                    col,
1489                    Some(LiteralValue::Empty),
1490                );
1491            }
1492            if let Some(mask) = self.spill_projection_history.get_mut(&addr) {
1493                // A clear leaves a projection mask, not a user blocker. Carry
1494                // it through later inserts even if the Empty was compacted.
1495                *mask = clears_projection;
1496            }
1497            if let Some(undo_ptr) = self.arrow_undo {
1498                // 1) Spill snapshot operations (computed overlay rect restore).
1499                let new_events = &unsafe { (&*capture_ptr).events() }[start_len..];
1500                let undo = unsafe { &mut *undo_ptr };
1501                self.engine
1502                    .record_spill_ops_into_arrow_undo(undo, new_events);
1503
1504                // 2) Delta/computed overlay single-cell deltas.
1505                let new_comp = self.engine.read_computed_overlay_cell(sheet, row, col);
1506                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1507                let row0 = row.saturating_sub(1);
1508                let col0 = col.saturating_sub(1);
1509                let delta_new_sem = (!clears_projection).then_some(value.clone());
1510                undo.record_delta_cell(sheet_id, row0, col0, delta_old_sem, delta_new_sem);
1511                undo.record_computed_cell(sheet_id, row0, col0, old_comp, new_comp);
1512            }
1513            Ok(())
1514        } else {
1515            self.engine
1516                .set_cell_value(sheet, row, col, value)
1517                .map_err(crate::engine::EditorError::from)
1518        }
1519    }
1520
1521    #[inline]
1522    pub fn set_cell_formula(
1523        &mut self,
1524        sheet: &str,
1525        row: u32,
1526        col: u32,
1527        ast: ASTNode,
1528    ) -> Result<(), crate::engine::EditorError> {
1529        self.engine.ensure_not_poisoned()?;
1530        if self.capture.is_some() {
1531            let old_value = self.engine.read_cell_value(sheet, row, col);
1532            let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1533            let addr = self.addr_for(sheet, row, col);
1534            let Some(capture_ptr) = self.capture else {
1535                return Err(crate::engine::EditorError::TransactionFailed {
1536                    reason: "action_with_logger: missing mutation capture".to_string(),
1537                });
1538            };
1539
1540            let admitted_formula = if self.engine.graph_admission_enabled() {
1541                let placement =
1542                    CellRef::new(addr.sheet_id, Coord::from_excel(row, col, true, true));
1543                let ingested = self.engine.ingest_pipeline().ingest_formula(
1544                    FormulaAstInput::Tree(ast.clone()),
1545                    placement,
1546                    None,
1547                )?;
1548                let admission = self.engine.graph.preview_formula_mutations(&[(
1549                    addr.sheet_id,
1550                    row,
1551                    col,
1552                    ingested.dep_plan.clone(),
1553                )])?;
1554                self.engine.preflight_graph_admission(admission)?;
1555                Some((ingested.ast_id, ingested.dep_plan))
1556            } else {
1557                None
1558            };
1559            if old_formula.is_none() {
1560                old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1561            }
1562            // History membership survives user writes. Only an eligible mask
1563            // still represents an untouched projection; a later literal must
1564            // be journaled so the formula's forward replay removes its delta.
1565            // Inverse Arrow replay already skips deltas over restored spills.
1566            let delta_old = if self.arrow_undo.is_some() {
1567                if old_formula.is_some() || self.spill_projection_history.get(&addr) == Some(&true)
1568                {
1569                    None
1570                } else {
1571                    Some(old_value.clone().unwrap_or(LiteralValue::Empty))
1572                }
1573            } else {
1574                None
1575            };
1576            let start_len = unsafe { (&*capture_ptr).len() };
1577
1578            // Safety: `capture_ptr` comes from a unique operation-local `&mut MutationCapture`.
1579            let capture = unsafe { &mut *capture_ptr };
1580            self.engine.edit_with_capture(capture, |editor| {
1581                if let Some((ast_id, plan)) = admitted_formula {
1582                    editor.set_cell_formula_with_prepared_plan(
1583                        addr,
1584                        ast.clone(),
1585                        old_value,
1586                        old_formula,
1587                        ast_id,
1588                        plan,
1589                    );
1590                } else {
1591                    editor.set_cell_formula_with_old_state(
1592                        addr,
1593                        ast.clone(),
1594                        old_value,
1595                        old_formula,
1596                    );
1597                }
1598            })?;
1599            self.engine.record_structural_change(StructuralScope::Cell {
1600                sheet: addr.sheet_id,
1601                row: addr.coord.row(),
1602                col: addr.coord.col(),
1603            });
1604
1605            self.remember_spill_clears(&unsafe { (&*capture_ptr).events() }[start_len..]);
1606            if let Some(mask) = self.spill_projection_history.get_mut(&addr) {
1607                *mask = false;
1608            }
1609            if let Some(undo_ptr) = self.arrow_undo {
1610                let new_events = &unsafe { (&*capture_ptr).events() }[start_len..];
1611                let undo = unsafe { &mut *undo_ptr };
1612                self.engine
1613                    .record_spill_ops_into_arrow_undo(undo, new_events);
1614                let delta_new: Option<LiteralValue> = None;
1615                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1616                let row0 = row.saturating_sub(1);
1617                let col0 = col.saturating_sub(1);
1618                undo.record_delta_cell(sheet_id, row0, col0, delta_old, delta_new);
1619            }
1620            Ok(())
1621        } else {
1622            self.engine
1623                .set_cell_formula(sheet, row, col, ast)
1624                .map_err(crate::engine::EditorError::from)
1625        }
1626    }
1627
1628    /// Resize a table within this action so rollback and history include its extent.
1629    pub fn resize_table(
1630        &mut self,
1631        name: &str,
1632        new_end_row: u32,
1633    ) -> Result<TableMetadata, crate::engine::EditorError> {
1634        self.resize_table_body(name, new_end_row, false)
1635    }
1636
1637    /// Resize and explicitly set the logical body state within this action.
1638    pub fn resize_table_body(
1639        &mut self,
1640        name: &str,
1641        new_end_row: u32,
1642        empty_body: bool,
1643    ) -> Result<TableMetadata, crate::engine::EditorError> {
1644        self.engine.ensure_not_poisoned()?;
1645        let range = self.engine.table_resize_range(name, new_end_row)?;
1646        if let Some(capture_ptr) = self.capture {
1647            self.engine
1648                .edit_with_capture(unsafe { &mut *capture_ptr }, |editor| {
1649                    editor.resize_table_body(name, range, empty_body)
1650                })??;
1651        } else {
1652            return self
1653                .engine
1654                .resize_table_body(name, new_end_row, empty_body)
1655                .map_err(Into::into);
1656        }
1657        Ok(self
1658            .engine
1659            .table_metadata(name)
1660            .expect("resized table exists"))
1661    }
1662
1663    #[inline]
1664    pub fn set_row_hidden(
1665        &mut self,
1666        sheet: &str,
1667        row_1based: u32,
1668        hidden: bool,
1669        source: RowVisibilitySource,
1670    ) -> Result<(), crate::engine::EditorError> {
1671        self.engine.ensure_not_poisoned()?;
1672        if self.capture.is_some() {
1673            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
1674            let row0 = Engine::<R>::normalize_row_1based(row_1based)?;
1675            let old_hidden = self
1676                .engine
1677                .row_visibility
1678                .get(&sheet_id)
1679                .map(|state| state.is_row_hidden(row0, Some(source)))
1680                .unwrap_or(false);
1681            if old_hidden == hidden {
1682                return Ok(());
1683            }
1684
1685            let _ = self
1686                .engine
1687                .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
1688
1689            let Some(capture_ptr) = self.capture else {
1690                return Err(crate::engine::EditorError::TransactionFailed {
1691                    reason: "action_with_logger: missing mutation capture".to_string(),
1692                });
1693            };
1694            unsafe { &mut *capture_ptr }.record(crate::engine::ChangeEvent::SetRowVisibility {
1695                sheet_id,
1696                row0,
1697                source,
1698                old_hidden,
1699                new_hidden: hidden,
1700            });
1701
1702            Ok(())
1703        } else {
1704            self.engine
1705                .set_row_hidden(sheet, row_1based, hidden, source)
1706        }
1707    }
1708
1709    #[inline]
1710    pub fn set_rows_hidden(
1711        &mut self,
1712        sheet: &str,
1713        start_row_1based: u32,
1714        end_row_1based: u32,
1715        hidden: bool,
1716        source: RowVisibilitySource,
1717    ) -> Result<(), crate::engine::EditorError> {
1718        self.engine.ensure_not_poisoned()?;
1719        if self.capture.is_some() {
1720            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
1721            let (start_row0, end_row0) =
1722                Engine::<R>::normalize_row_range_1based(start_row_1based, end_row_1based)?;
1723
1724            let Some(capture_ptr) = self.capture else {
1725                return Err(crate::engine::EditorError::TransactionFailed {
1726                    reason: "action_with_logger: missing mutation capture".to_string(),
1727                });
1728            };
1729            let capture = unsafe { &mut *capture_ptr };
1730
1731            for row0 in start_row0..=end_row0 {
1732                let old_hidden = self
1733                    .engine
1734                    .row_visibility
1735                    .get(&sheet_id)
1736                    .map(|state| state.is_row_hidden(row0, Some(source)))
1737                    .unwrap_or(false);
1738                if old_hidden == hidden {
1739                    continue;
1740                }
1741
1742                let _ = self
1743                    .engine
1744                    .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
1745
1746                capture.record(crate::engine::ChangeEvent::SetRowVisibility {
1747                    sheet_id,
1748                    row0,
1749                    source,
1750                    old_hidden,
1751                    new_hidden: hidden,
1752                });
1753            }
1754
1755            Ok(())
1756        } else {
1757            self.engine
1758                .set_rows_hidden(sheet, start_row_1based, end_row_1based, hidden, source)
1759        }
1760    }
1761
1762    #[inline]
1763    pub fn insert_rows(
1764        &mut self,
1765        sheet: &str,
1766        before: u32,
1767        count: u32,
1768    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1769        self.engine.ensure_not_poisoned()?;
1770        if count == 0 {
1771            return Ok(crate::engine::ShiftSummary::default());
1772        }
1773        if self.capture.is_some() {
1774            let Some(capture_ptr) = self.capture else {
1775                return Err(crate::engine::EditorError::TransactionFailed {
1776                    reason: "action_atomic: missing mutation capture".to_string(),
1777                });
1778            };
1779
1780            let before0 = before.saturating_sub(1);
1781            self.engine.check_insert_room(sheet, before0, count, true)?;
1782            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1783            let occupancy = self.engine.structural_row_occupancy(sheet, sheet_id);
1784            let affected_region = Engine::<R>::structural_row_region(sheet_id, before0);
1785            let mut spill_masks = self.spill_history_masks_before_insert(sheet_id, before0, true);
1786
1787            // Capture/mirror the clear before the structural graph and Arrow
1788            // moves, and journal it before the insert's inverse Arrow operation.
1789            let cleared_spill_cells = self.engine.clear_spills_before_structural_edit(
1790                sheet_id,
1791                before0,
1792                true,
1793                Some(unsafe { &mut *capture_ptr }),
1794                self.arrow_undo.map(|ptr| unsafe { &mut *ptr }),
1795            )?;
1796            spill_masks.extend(cleared_spill_cells.iter().copied());
1797
1798            // Graph structural insert (logged) - no snapshot bump.
1799            let summary = {
1800                let capture = unsafe { &mut *capture_ptr };
1801                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
1802                    Ok(crate::engine::ShiftSummary::default());
1803                self.engine.edit_with_capture(capture, |editor| {
1804                    editor.set_structural_occupancy(occupancy);
1805                    out = editor.insert_rows(sheet_id, before0, count);
1806                })?;
1807                out?
1808            };
1809
1810            // Arrow insert (truth) + undo op.
1811            self.engine.ensure_arrow_sheet(sheet);
1812            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
1813                asheet.insert_rows(before0 as usize, count as usize);
1814            }
1815            for cell in cleared_spill_cells {
1816                self.spill_projection_history.entry(cell).or_insert(true);
1817            }
1818            self.shift_spill_history(sheet_id, before0, count, true);
1819            if let Some(undo_ptr) = self.arrow_undo {
1820                unsafe { &mut *undo_ptr }.record_insert_rows(sheet_id, before0, count);
1821            }
1822            self.engine
1823                .purge_derived_formats_after_row(sheet_id, before0);
1824            self.engine
1825                .shift_row_visibility_insert(sheet_id, before0, count);
1826            self.engine.mark_moved_formula_vertices_dirty(&summary);
1827            if let Some(undo_ptr) = self.arrow_undo {
1828                self.engine.journal_structural_computed_clear(
1829                    sheet,
1830                    before0 as usize,
1831                    true,
1832                    unsafe { &mut *undo_ptr },
1833                );
1834            }
1835            self.engine
1836                .clear_computed_overlay_after_row(sheet, before0 as usize);
1837            self.engine.restore_shifted_spill_tombstones(
1838                spill_masks,
1839                before0,
1840                count,
1841                true,
1842                true,
1843                self.arrow_undo.map(|ptr| unsafe { &mut *ptr }),
1844            );
1845            self.engine
1846                .record_structural_change(StructuralScope::Region(affected_region));
1847            Ok(summary)
1848        } else {
1849            self.engine.insert_rows(sheet, before, count)
1850        }
1851    }
1852
1853    #[inline]
1854    pub fn delete_rows(
1855        &mut self,
1856        sheet: &str,
1857        start: u32,
1858        count: u32,
1859    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1860        self.engine.ensure_not_poisoned()?;
1861        if count == 0 {
1862            return Ok(crate::engine::ShiftSummary::default());
1863        }
1864        if self.atomic_policy {
1865            return Err(crate::engine::EditorError::TransactionUnsupported {
1866                reason:
1867                    "delete_rows is not supported inside atomic actions (conservative rollback policy)"
1868                        .to_string(),
1869            });
1870        }
1871        self.engine.delete_rows(sheet, start, count)
1872    }
1873
1874    #[inline]
1875    pub fn insert_columns(
1876        &mut self,
1877        sheet: &str,
1878        before: u32,
1879        count: u32,
1880    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1881        self.engine.ensure_not_poisoned()?;
1882        if count == 0 {
1883            return Ok(crate::engine::ShiftSummary::default());
1884        }
1885        if self.capture.is_some() {
1886            let Some(capture_ptr) = self.capture else {
1887                return Err(crate::engine::EditorError::TransactionFailed {
1888                    reason: "action_atomic: missing mutation capture".to_string(),
1889                });
1890            };
1891
1892            let before0 = before.saturating_sub(1);
1893            self.engine
1894                .check_insert_room(sheet, before0, count, false)?;
1895            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1896            let occupancy = self.engine.structural_column_occupancy();
1897            let affected_region = Engine::<R>::structural_col_region(sheet_id, before0);
1898            let mut spill_masks = self.spill_history_masks_before_insert(sheet_id, before0, false);
1899
1900            // Capture/mirror the clear before the structural graph and Arrow
1901            // moves, and journal it before the insert's inverse Arrow operation.
1902            let cleared_spill_cells = self.engine.clear_spills_before_structural_edit(
1903                sheet_id,
1904                before0,
1905                false,
1906                Some(unsafe { &mut *capture_ptr }),
1907                self.arrow_undo.map(|ptr| unsafe { &mut *ptr }),
1908            )?;
1909            spill_masks.extend(cleared_spill_cells.iter().copied());
1910
1911            let summary = {
1912                let capture = unsafe { &mut *capture_ptr };
1913                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
1914                    Ok(crate::engine::ShiftSummary::default());
1915                self.engine.edit_with_capture(capture, |editor| {
1916                    editor.set_structural_occupancy(occupancy);
1917                    out = editor.insert_columns(sheet_id, before0, count);
1918                })?;
1919                out?
1920            };
1921
1922            self.engine.ensure_arrow_sheet(sheet);
1923            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
1924                asheet.insert_columns(before0 as usize, count as usize);
1925            }
1926            for cell in cleared_spill_cells {
1927                self.spill_projection_history.entry(cell).or_insert(true);
1928            }
1929            self.shift_spill_history(sheet_id, before0, count, false);
1930            if let Some(undo_ptr) = self.arrow_undo {
1931                unsafe { &mut *undo_ptr }.record_insert_cols(sheet_id, before0, count);
1932            }
1933            self.engine
1934                .purge_derived_formats_after_col(sheet_id, before0);
1935            self.engine.mark_moved_formula_vertices_dirty(&summary);
1936            if let Some(undo_ptr) = self.arrow_undo {
1937                self.engine.journal_structural_computed_clear(
1938                    sheet,
1939                    before0 as usize,
1940                    false,
1941                    unsafe { &mut *undo_ptr },
1942                );
1943            }
1944            self.engine
1945                .clear_computed_overlay_after_col(sheet, before0 as usize);
1946            self.engine.restore_shifted_spill_tombstones(
1947                spill_masks,
1948                before0,
1949                count,
1950                false,
1951                true,
1952                self.arrow_undo.map(|ptr| unsafe { &mut *ptr }),
1953            );
1954            self.engine
1955                .record_structural_change(StructuralScope::Region(affected_region));
1956            Ok(summary)
1957        } else {
1958            self.engine.insert_columns(sheet, before, count)
1959        }
1960    }
1961
1962    #[inline]
1963    pub fn delete_columns(
1964        &mut self,
1965        sheet: &str,
1966        start: u32,
1967        count: u32,
1968    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1969        self.engine.ensure_not_poisoned()?;
1970        if count == 0 {
1971            return Ok(crate::engine::ShiftSummary::default());
1972        }
1973        if self.atomic_policy {
1974            return Err(crate::engine::EditorError::TransactionUnsupported {
1975                reason:
1976                    "delete_columns is not supported inside atomic actions (conservative rollback policy)"
1977                        .to_string(),
1978            });
1979        }
1980        self.engine.delete_columns(sheet, start, count)
1981    }
1982
1983    /// Start an action from within an action.
1984    ///
1985    /// Nested actions are currently disallowed (ticket 614), so this will return a
1986    /// `EditorError::TransactionFailed` while an outer action is active.
1987    #[inline]
1988    pub fn action<T>(
1989        &mut self,
1990        name: impl AsRef<str>,
1991        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
1992    ) -> Result<T, crate::engine::EditorError> {
1993        self.engine.ensure_not_poisoned()?;
1994        self.engine.action(name, f)
1995    }
1996}
1997
1998struct ActionDepthGuard<'a, R> {
1999    engine: *mut Engine<R>,
2000    _marker: std::marker::PhantomData<&'a mut Engine<R>>,
2001}
2002
2003impl<'a, R> Drop for ActionDepthGuard<'a, R> {
2004    fn drop(&mut self) {
2005        // Safety: the guard is created from a unique `&mut Engine` borrow and lives no longer
2006        // than the surrounding `Engine::action` call.
2007        unsafe {
2008            let e = &mut *self.engine;
2009            e.action_depth = e.action_depth.saturating_sub(1);
2010        }
2011    }
2012}
2013
2014#[derive(Default)]
2015struct SourceCache {
2016    scalars: FxHashMap<(String, Option<u64>), LiteralValue>,
2017    tables: FxHashMap<(String, Option<u64>), Arc<dyn crate::traits::Table>>,
2018    /// Materialized views of external range sources: one build per source
2019    /// version, shared by every formula that reads it.
2020    external_views: FxHashMap<(String, Option<u64>), RangeView<'static>>,
2021}
2022
2023#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2024struct VisibilityMaskCacheKey {
2025    sheet_id: SheetId,
2026    start_row0: u32,
2027    end_row0: u32,
2028    mode: VisibilityMaskMode,
2029    version: u64,
2030}
2031
2032#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2033enum StructuralScope {
2034    Cell { sheet: SheetId, row: u32, col: u32 },
2035    Region(Region),
2036    Sheet(SheetId),
2037    RemovedSheet(SheetId),
2038    OpaqueGlobal,
2039    AllSheets,
2040}
2041
2042/// Anchors blocked by a formula or another spill, indexed by the blocking
2043/// cell so that a single-cell change finds its waiters in O(1).
2044#[derive(Default)]
2045struct SpillBlockerWaiters {
2046    by_cell: FxHashMap<CellRef, Vec<VertexId>>,
2047    by_anchor: FxHashMap<VertexId, CellRef>,
2048}
2049
2050impl SpillBlockerWaiters {
2051    /// Largest region scanned cell by cell; a larger one scans the waiters.
2052    const REGION_PROBE_CELLS: u64 = 4096;
2053
2054    fn is_empty(&self) -> bool {
2055        self.by_anchor.is_empty()
2056    }
2057
2058    fn wait(&mut self, anchor: VertexId, blocker: CellRef) {
2059        match self.by_anchor.insert(anchor, blocker) {
2060            Some(old) if old == blocker => return,
2061            Some(old) => self.unlink(anchor, old),
2062            None => {}
2063        }
2064        self.by_cell.entry(blocker).or_default().push(anchor);
2065    }
2066
2067    fn forget(&mut self, anchor: VertexId) {
2068        if self.by_anchor.is_empty() {
2069            return;
2070        }
2071        if let Some(old) = self.by_anchor.remove(&anchor) {
2072            self.unlink(anchor, old);
2073        }
2074    }
2075
2076    fn unlink(&mut self, anchor: VertexId, cell: CellRef) {
2077        if let Some(waiters) = self.by_cell.get_mut(&cell) {
2078            waiters.retain(|&v| v != anchor);
2079            if waiters.is_empty() {
2080                self.by_cell.remove(&cell);
2081            }
2082        }
2083    }
2084
2085    /// Remove and return the anchors waiting on a cell inside `scope`.
2086    fn take_affected(&mut self, scope: StructuralScope) -> Vec<VertexId> {
2087        let cells: Vec<CellRef> = match scope {
2088            StructuralScope::Cell { sheet, row, col } => {
2089                match Coord::try_new(row, col, true, true) {
2090                    Ok(coord) if self.by_cell.contains_key(&CellRef::new(sheet, coord)) => {
2091                        vec![CellRef::new(sheet, coord)]
2092                    }
2093                    _ => Vec::new(),
2094                }
2095            }
2096            StructuralScope::Region(region) => {
2097                let (rows, cols) = region.axis_ranges();
2098                let (r0, r1) = rows.query_bounds();
2099                let (c0, c1) = cols.query_bounds();
2100                // Widen before `+ 1`: an unbounded axis (every structural edit
2101                // records `rows_from` / `cols_from`) spans 0..=u32::MAX, and
2102                // the u32 sum wrapped to an area of 0.
2103                let area = (u64::from(r1.saturating_sub(r0)) + 1)
2104                    .saturating_mul(u64::from(c1.saturating_sub(c0)) + 1);
2105                if area <= Self::REGION_PROBE_CELLS {
2106                    let sheet = region.sheet_id();
2107                    // A small region can still lie past the grid; no waiter
2108                    // is registered there, so those points are skipped.
2109                    (r0..=r1)
2110                        .flat_map(|r| (c0..=c1).map(move |c| (r, c)))
2111                        .filter_map(|(r, c)| Coord::try_new(r, c, true, true).ok())
2112                        .map(|coord| CellRef::new(sheet, coord))
2113                        .filter(|cell| self.by_cell.contains_key(cell))
2114                        .collect()
2115                } else {
2116                    self.by_cell
2117                        .keys()
2118                        .filter(|cell| {
2119                            region.intersects(&Region::point(
2120                                cell.sheet_id,
2121                                cell.coord.row(),
2122                                cell.coord.col(),
2123                            ))
2124                        })
2125                        .copied()
2126                        .collect()
2127                }
2128            }
2129            StructuralScope::Sheet(sheet) | StructuralScope::RemovedSheet(sheet) => self
2130                .by_cell
2131                .keys()
2132                .filter(|cell| cell.sheet_id == sheet)
2133                .copied()
2134                .collect(),
2135            StructuralScope::OpaqueGlobal | StructuralScope::AllSheets => {
2136                self.by_cell.keys().copied().collect()
2137            }
2138        };
2139        let mut woken = Vec::new();
2140        for cell in cells {
2141            if let Some(waiters) = self.by_cell.remove(&cell) {
2142                for anchor in waiters {
2143                    self.by_anchor.remove(&anchor);
2144                    woken.push(anchor);
2145                }
2146            }
2147        }
2148        woken
2149    }
2150}
2151
2152#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
2153enum LoggedEditImpact {
2154    NoOp,
2155    DataOnly,
2156    Topology,
2157}
2158
2159#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2160enum LoggedEditDirection {
2161    Original,
2162    InverseReplay,
2163    ForwardReplay,
2164}
2165
2166#[derive(Clone, Copy)]
2167struct InvalidationBaseline {
2168    snapshot_id: u64,
2169    topology_epoch: u64,
2170}
2171
2172struct SourceCacheSession {
2173    cache: Arc<std::sync::RwLock<SourceCache>>,
2174}
2175
2176impl Drop for SourceCacheSession {
2177    fn drop(&mut self) {
2178        if let Ok(mut g) = self.cache.write() {
2179            *g = SourceCache::default();
2180        }
2181    }
2182}
2183
2184#[derive(Debug)]
2185#[non_exhaustive]
2186pub struct EvalResult {
2187    pub computed_vertices: usize,
2188    pub cycle_errors: usize,
2189    pub elapsed: std::time::Duration,
2190}
2191
2192#[derive(Clone, Debug, PartialEq, Eq)]
2193#[non_exhaustive]
2194pub struct TableMetadata {
2195    pub name: String,
2196    pub sheet: String,
2197    pub start_row: u32,
2198    pub start_col: u32,
2199    pub end_row: u32,
2200    pub end_col: u32,
2201    pub header_row: bool,
2202    pub headers: Vec<String>,
2203    pub totals_row: bool,
2204    /// The reserved physical body row represents zero logical records.
2205    pub empty_body: bool,
2206}
2207
2208/// Read-only engine counters used by benchmark/instrumentation tooling.
2209///
2210/// These counters are deliberately observational: collecting them must not mutate engine state or
2211/// alter formula evaluation semantics.
2212///
2213/// The `formula_plane_*` counters are always `0`: FormulaPlane spans were removed and the
2214/// dependency authority is the only runtime path. They are kept for source compatibility.
2215#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
2216#[non_exhaustive]
2217pub struct EngineBaselineStats {
2218    pub graph_vertex_count: usize,
2219    pub graph_formula_vertex_count: usize,
2220    pub graph_edge_count: usize,
2221    pub dirty_vertex_count: usize,
2222    pub evaluation_vertex_count: usize,
2223    pub formula_ast_root_count: usize,
2224    pub formula_ast_node_count: usize,
2225    pub staged_formula_count: usize,
2226    pub formula_plane_active_span_count: usize,
2227    pub formula_plane_producer_result_entries: usize,
2228    pub formula_plane_consumer_read_entries: usize,
2229    pub formula_plane_mixed_topology_cache_builds: u64,
2230    pub formula_plane_mixed_topology_cache_hits: u64,
2231    pub formula_plane_mixed_topology_cache_overflows: u64,
2232    pub formula_plane_dirty_pending_events: usize,
2233    pub formula_plane_dirty_region_events_recorded: u64,
2234    pub formula_plane_dirty_span_region_events_recorded: u64,
2235    pub formula_plane_dirty_whole_span_seeds_recorded: u64,
2236    pub formula_plane_dirty_global_invalidations: u64,
2237    pub formula_plane_structural_span_candidates: u64,
2238    pub formula_plane_cycle_member_span_demotions: u64,
2239    pub formula_plane_array_result_span_demotions: u64,
2240    /// Members of exactly converged iterative SCCs currently retained across
2241    /// recalcs (#368).
2242    pub retained_scc_members: usize,
2243}
2244
2245#[derive(Debug, Clone, Default)]
2246#[non_exhaustive]
2247pub struct VirtualDepTelemetry {
2248    pub candidate_vertices_total: usize,
2249    pub vdeps_vertices_total: usize,
2250    pub vdeps_edges_total: usize,
2251    pub builder_elapsed_ms_total: u128,
2252    pub schedule_virtual_passes: usize,
2253    pub schedule_static_passes: usize,
2254    pub schedule_cache_hits: usize,
2255    pub schedule_cache_misses: usize,
2256    pub reused_schedule_vertices_total: usize,
2257    pub replan_iterations: usize,
2258    pub changed_vdeps_total: usize,
2259    pub bailout_reason: Option<&'static str>,
2260    pub fallback_mode_activations: u64,
2261}
2262
2263/// Per-recalc telemetry for SCC evaluation under `CycleDetection::Runtime`
2264/// (spec `formualizer-cycle-semantics-spec.md` §10).
2265///
2266/// Collection is unconditional: SCC tasks are rare relative to ordinary
2267/// vertex evaluation and the counters are a handful of integer adds per
2268/// task, so no config flag gates them (unlike [`VirtualDepTelemetry`],
2269/// which pays per-schedule costs). Counters reset at the start of every
2270/// evaluation request.
2271#[derive(Debug, Clone, Default, PartialEq)]
2272#[non_exhaustive]
2273pub struct CycleTelemetry {
2274    /// SCC tasks executed (static SCCs that reached Runtime evaluation).
2275    pub static_sccs: usize,
2276    /// SCC tasks whose live subgraph was acyclic — values produced.
2277    pub phantom_sccs: usize,
2278    /// Distinct live cycles witnessed across all SCC tasks.
2279    pub live_cycles_witnessed: usize,
2280    /// Cells stamped `#CIRC!` by Runtime SCC tasks.
2281    pub circ_cells_stamped: usize,
2282    /// Evaluation sweeps over (subsets of) SCC members, totalled across tasks
2283    /// (pass 1 included).
2284    pub settle_passes_total: usize,
2285    /// Largest pass count any single SCC task needed.
2286    pub max_passes_single_scc: usize,
2287    /// SCC tasks that entered iterative calculation (`CyclePolicy::Iterate`
2288    /// with a witnessed live cycle). RFC #113, Stage 3.
2289    pub iterated_sccs: usize,
2290    /// Iterating SCC tasks that stopped because every member passed the
2291    /// spec-§6 convergence test.
2292    pub converged_sccs: usize,
2293    /// SCC tasks that stopped at a pass cap. Under `CyclePolicy::Iterate`
2294    /// this is the Excel `max_iterations` cap (NOT an error — last values
2295    /// are kept; includes the no-convergence-test `max_iterations: 1`
2296    /// contract). Under `CyclePolicy::Error` it is the defensive acyclic
2297    /// settle cap (|SCC| + 2), which only a bug can hit.
2298    pub capped_sccs: usize,
2299    /// Largest `|Δ|` observed in any member's final-pass convergence
2300    /// comparison across iterating SCC tasks (numeric-class members only).
2301    /// `0.0` when no comparison ran (e.g. `max_iterations: 1`).
2302    pub max_abs_delta_at_stop: f64,
2303    /// Identical-bit NaN vs NaN member comparisons that were treated as
2304    /// converged (spec §6 NaN rule).
2305    pub nan_converged: usize,
2306    /// Retained iterative SCCs (#368) that had no dirty member at request begin and were therefore not
2307    /// re-run: their last exact fixed point is served as-is. Counted at
2308    /// request begin, so a demand-driven request that never reaches a
2309    /// retained SCC still reports it as reused.
2310    pub reused_sccs: usize,
2311    /// Members of the SCCs counted in `reused_sccs`.
2312    pub reused_scc_members: usize,
2313    /// Total wall-clock time spent inside Runtime SCC tasks.
2314    pub elapsed_ms: u128,
2315}
2316
2317#[derive(Debug, Clone, Copy)]
2318struct ScheduleBuildMeta {
2319    candidate_vertices: usize,
2320    vdeps_vertices: usize,
2321    vdeps_edges: usize,
2322    builder_elapsed_ms: u128,
2323    used_virtual_schedule: bool,
2324    schedule_cache_hit: bool,
2325    schedule_cache_eligible: bool,
2326}
2327
2328#[cfg(any(test, feature = "benchmark_internal"))]
2329#[doc(hidden)]
2330#[derive(Debug, Clone, Default)]
2331pub struct RecalcReuseProbe {
2332    pub schedule_requests: usize,
2333    pub schedule_cache_hits: usize,
2334    pub schedule_cache_misses: usize,
2335    pub schedule_cache_ineligible: usize,
2336    pub schedule_builds: usize,
2337    /// Program 3 plan reuse: misses served by restricting the base schedule.
2338    pub schedule_base_restrictions: usize,
2339    pub schedule_shared_handles: usize,
2340    pub schedule_retained_bytes: usize,
2341    pub legacy_target_requests: usize,
2342    pub target_schedule_builds: usize,
2343    pub demand_builds: usize,
2344    pub demand_vertices: usize,
2345    pub demand_clean_formulas: usize,
2346    pub demand_explicit_edges: usize,
2347    pub demand_virtual_builder_calls: usize,
2348}
2349
2350#[cfg(any(test, feature = "benchmark_internal"))]
2351fn schedule_probe_retained_bytes(schedule: &crate::engine::Schedule) -> usize {
2352    fn vector_bytes<T>(values: &Vec<T>) -> usize {
2353        values.capacity() * std::mem::size_of::<T>()
2354    }
2355
2356    [
2357        vector_bytes(&schedule.units),
2358        vector_bytes(&schedule.layers),
2359        vector_bytes(&schedule.cycles),
2360    ]
2361    .into_iter()
2362    .chain(
2363        schedule
2364            .layers
2365            .iter()
2366            .map(|layer| vector_bytes(&layer.vertices)),
2367    )
2368    .chain(schedule.cycles.iter().map(vector_bytes))
2369    .sum()
2370}
2371
2372#[derive(Debug, Clone)]
2373struct CachedScheduleEntry {
2374    topology_epoch: u64,
2375    /// Authority `(store revision, rev.dyn)` the schedule was planned from
2376    /// (design §8.4; always 0 without `unified_authority`).
2377    authority_revision: (u64, u64),
2378    /// The request's vertex list as runs of consecutive ids (formula ids
2379    /// come in column runs, so a whole-workbook request is a few runs).
2380    candidate_vertices: VertexIdRuns,
2381    schedule: Arc<crate::engine::scheduler::Schedule>,
2382}
2383
2384/// A vertex list stored as `(first id, run length)` runs of consecutive
2385/// ids, in list order.
2386#[derive(Debug, Clone, Default)]
2387struct VertexIdRuns(Vec<(u32, u32)>);
2388
2389impl VertexIdRuns {
2390    fn from_slice(ids: &[VertexId]) -> Self {
2391        let mut runs: Vec<(u32, u32)> = Vec::new();
2392        for v in ids {
2393            match runs.last_mut() {
2394                Some((first, len)) if first.checked_add(*len) == Some(v.0) => *len += 1,
2395                _ => runs.push((v.0, 1)),
2396            }
2397        }
2398        runs.shrink_to_fit();
2399        Self(runs)
2400    }
2401
2402    fn equals(&self, ids: &[VertexId]) -> bool {
2403        let mut rest = ids;
2404        for &(first, len) in &self.0 {
2405            let len = len as usize;
2406            if rest.len() < len {
2407                return false;
2408            }
2409            let (head, tail) = rest.split_at(len);
2410            if head
2411                .iter()
2412                .enumerate()
2413                .any(|(i, v)| v.0 != first.wrapping_add(i as u32))
2414            {
2415                return false;
2416            }
2417            rest = tail;
2418        }
2419        rest.is_empty()
2420    }
2421
2422    fn heap_bytes(&self) -> usize {
2423        self.0.capacity() * std::mem::size_of::<(u32, u32)>()
2424    }
2425
2426    /// The number of ids.
2427    fn len(&self) -> usize {
2428        self.0.iter().map(|&(_, len)| len as usize).sum()
2429    }
2430}
2431
2432#[cfg(test)]
2433mod vertex_id_runs_tests {
2434    use super::{VertexId, VertexIdRuns};
2435
2436    #[test]
2437    fn runs_compare_like_the_list() {
2438        let ids = |v: &[u32]| v.iter().map(|&i| VertexId(i)).collect::<Vec<_>>();
2439        let list = ids(&[5, 6, 7, 2, 3, 9, 10, 10]);
2440        let runs = VertexIdRuns::from_slice(&list);
2441        assert_eq!(runs.0, vec![(5, 3), (2, 2), (9, 2), (10, 1)]);
2442        assert!(runs.equals(&list));
2443        assert!(!runs.equals(&list[..7]));
2444        assert!(!runs.equals(&ids(&[5, 6, 7, 2, 3, 9, 10, 11])));
2445        assert!(!runs.equals(&ids(&[5, 6, 7, 2, 3, 9, 10, 10, 11])));
2446        assert!(VertexIdRuns::from_slice(&[]).equals(&[]));
2447        assert!(!VertexIdRuns::from_slice(&[]).equals(&list));
2448        let edge = ids(&[u32::MAX - 1, u32::MAX, 0]);
2449        assert!(VertexIdRuns::from_slice(&edge).equals(&edge));
2450    }
2451}
2452
2453/// Uncacheable requests keep their schedule inline without a shared allocation.
2454#[path = "recalc_job.rs"]
2455mod job_driver;
2456pub(super) use job_driver::HeldRecalcSchedule;
2457
2458pub(super) enum EvaluationSchedule {
2459    Owned(crate::engine::scheduler::Schedule),
2460    Shared(Arc<crate::engine::scheduler::Schedule>),
2461}
2462
2463impl std::ops::Deref for EvaluationSchedule {
2464    type Target = crate::engine::scheduler::Schedule;
2465
2466    fn deref(&self) -> &Self::Target {
2467        match self {
2468            Self::Owned(schedule) => schedule,
2469            Self::Shared(schedule) => schedule,
2470        }
2471    }
2472}
2473
2474type ScheduleBuildOutput = (
2475    crate::engine::scheduler::Schedule,
2476    FxHashMap<VertexId, Vec<VertexId>>,
2477    ScheduleBuildMeta,
2478);
2479
2480type EvaluationScheduleBuildOutput = (
2481    EvaluationSchedule,
2482    FxHashMap<VertexId, Vec<VertexId>>,
2483    ScheduleBuildMeta,
2484);
2485
2486/// Opaque, revision-bound recalculation recipe.
2487#[derive(Debug)]
2488pub struct RecalcPlan {
2489    key: RecalcPlanKey,
2490    kind: RecalcPlanKind,
2491}
2492
2493#[derive(Debug)]
2494struct RecalcPlanKey {
2495    engine_token: Arc<()>,
2496    revisions: PlanningRevisionSnapshot,
2497}
2498
2499#[derive(Clone, Debug, PartialEq, Eq)]
2500struct PlanningRevisionSnapshot {
2501    engine_topology_epoch: u64,
2502    graph_topology_revision: u64,
2503    staged: u64,
2504    symbols: u64,
2505    semantic: u64,
2506    provider: Option<u64>,
2507    deterministic_mode: crate::engine::DeterministicMode,
2508    budgets: crate::engine::EvaluationBudgets,
2509}
2510
2511#[derive(Debug)]
2512enum RecalcPlanKind {
2513    CompatibilityFull {
2514        schedule: crate::engine::Schedule,
2515        has_dynamic_refs: bool,
2516    },
2517    Target {
2518        targets: Vec<crate::engine::EvaluationTarget>,
2519        scope: crate::engine::PrepareScope,
2520        topology: RecalcTopology,
2521        dynamic_policy: DynamicPlanPolicy,
2522    },
2523}
2524
2525#[derive(Debug)]
2526enum RecalcTopology {
2527    RunLocalRecipe,
2528    Workbook,
2529}
2530
2531#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2532enum DynamicPlanPolicy {
2533    BoundedTargetReplan,
2534}
2535
2536impl RecalcPlan {
2537    /// Returns the retained compatibility schedule depth. Target plans retain a
2538    /// run-local recipe rather than a schedule, so their layer count is zero.
2539    pub fn layer_count(&self) -> usize {
2540        match &self.kind {
2541            RecalcPlanKind::CompatibilityFull { schedule, .. } => schedule.layers.len(),
2542            RecalcPlanKind::Target { .. } => 0,
2543        }
2544    }
2545
2546    pub fn has_dynamic_refs(&self) -> bool {
2547        match &self.kind {
2548            RecalcPlanKind::CompatibilityFull {
2549                has_dynamic_refs, ..
2550            } => *has_dynamic_refs,
2551            RecalcPlanKind::Target { .. } => false,
2552        }
2553    }
2554
2555    #[cfg(test)]
2556    pub(crate) fn force_stale_reasons_for_test(
2557        &mut self,
2558        reasons: &[formualizer_common::PlanStaleReason],
2559    ) {
2560        use formualizer_common::PlanStaleReason;
2561        for reason in reasons {
2562            match reason {
2563                PlanStaleReason::Engine => {
2564                    self.key.engine_token = Arc::new(());
2565                }
2566                PlanStaleReason::Provider => {
2567                    self.key.revisions.provider = Some(
2568                        self.key
2569                            .revisions
2570                            .provider
2571                            .unwrap_or_default()
2572                            .wrapping_add(1),
2573                    );
2574                }
2575                PlanStaleReason::Semantic => {
2576                    self.key.revisions.semantic = self.key.revisions.semantic.wrapping_add(1);
2577                }
2578                PlanStaleReason::Budget => {
2579                    let current = self.key.revisions.budgets.work.max_work_units;
2580                    self.key.revisions.budgets.work.max_work_units =
2581                        Some(current.unwrap_or_default().wrapping_add(1));
2582                }
2583                PlanStaleReason::Staged => {
2584                    self.key.revisions.staged = self.key.revisions.staged.wrapping_add(1);
2585                }
2586                PlanStaleReason::Symbols => {
2587                    self.key.revisions.symbols = self.key.revisions.symbols.wrapping_add(1);
2588                }
2589                PlanStaleReason::Graph => {
2590                    self.key.revisions.graph_topology_revision =
2591                        self.key.revisions.graph_topology_revision.wrapping_add(1);
2592                }
2593                _ => {}
2594            }
2595        }
2596    }
2597}
2598
2599#[cfg(any(test, feature = "test-support"))]
2600pub(crate) mod criteria_mask_test_hooks {
2601    use std::cell::Cell;
2602
2603    thread_local! {
2604        static MASK_CALLS_ROWS: Cell<(usize, usize)> = const { Cell::new((0, 0)) };
2605        static TEXT_SEGMENTS_TOTAL: Cell<usize> = const { Cell::new(0) };
2606        static TEXT_SEGMENTS_ALL_NULL: Cell<usize> = const { Cell::new(0) };
2607    }
2608
2609    pub(crate) fn take_mask_work() -> (usize, usize) {
2610        MASK_CALLS_ROWS.with(|c| c.replace((0, 0)))
2611    }
2612
2613    pub(crate) fn note_mask(rows: usize) {
2614        MASK_CALLS_ROWS.with(|c| {
2615            let (calls, work) = c.get();
2616            c.set((calls + 1, work + rows));
2617        });
2618    }
2619
2620    pub fn reset_text_segment_counters() {
2621        TEXT_SEGMENTS_TOTAL.with(|c| c.set(0));
2622        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(0));
2623    }
2624
2625    pub fn text_segment_counters() -> (usize, usize) {
2626        let a = TEXT_SEGMENTS_TOTAL.with(|c| c.get());
2627        let b = TEXT_SEGMENTS_ALL_NULL.with(|c| c.get());
2628        (a, b)
2629    }
2630
2631    pub(crate) fn inc_total() {
2632        TEXT_SEGMENTS_TOTAL.with(|c| c.set(c.get() + 1));
2633    }
2634    pub(crate) fn inc_all_null() {
2635        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(c.get() + 1));
2636    }
2637}
2638
2639#[cfg(test)]
2640pub(crate) mod visibility_mask_test_hooks {
2641    use std::cell::Cell;
2642
2643    thread_local! {
2644        static HITS: Cell<usize> = const { Cell::new(0) };
2645        static MISSES: Cell<usize> = const { Cell::new(0) };
2646        static EVICTIONS: Cell<usize> = const { Cell::new(0) };
2647    }
2648
2649    pub fn reset() {
2650        HITS.with(|c| c.set(0));
2651        MISSES.with(|c| c.set(0));
2652        EVICTIONS.with(|c| c.set(0));
2653    }
2654
2655    pub fn counters() -> (usize, usize, usize) {
2656        let hits = HITS.with(|c| c.get());
2657        let misses = MISSES.with(|c| c.get());
2658        let evictions = EVICTIONS.with(|c| c.get());
2659        (hits, misses, evictions)
2660    }
2661
2662    pub(crate) fn inc_hit() {
2663        HITS.with(|c| c.set(c.get() + 1));
2664    }
2665
2666    pub(crate) fn inc_miss() {
2667        MISSES.with(|c| c.set(c.get() + 1));
2668    }
2669
2670    pub(crate) fn inc_eviction() {
2671        EVICTIONS.with(|c| c.set(c.get() + 1));
2672    }
2673}
2674
2675fn is_numeric_text_equality(pred: &crate::args::CriteriaPredicate) -> bool {
2676    match pred {
2677        crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(text)) => {
2678            text.trim().parse::<f64>().is_ok_and(f64::is_finite)
2679        }
2680        _ => false,
2681    }
2682}
2683
2684/// Resolve the null (non-number) rows of an `=n` / `<>n` criteria mask over
2685/// one row segment: numeric text compares by value, every other non-number
2686/// cell (blank, boolean, error) is unequal. Segments without text stay
2687/// vectorized.
2688fn fill_numeric_equality_nulls(
2689    view: &RangeView<'_>,
2690    row_start: usize,
2691    row_len: usize,
2692    col_in_view: usize,
2693    mask: &arrow_array::BooleanArray,
2694    n: f64,
2695    ne: bool,
2696) -> Option<arrow_array::BooleanArray> {
2697    use arrow_array::Array as _;
2698    let texts = view.slice_lowered_text(row_start, row_len);
2699    let text = texts.get(col_in_view).and_then(|t| t.as_ref());
2700    match text {
2701        None => {
2702            if !ne {
2703                // Nulls are already "no match".
2704                return Some(mask.clone());
2705            }
2706            let nulls = arrow::compute::is_null(mask).ok()?;
2707            crate::compute_prelude::boolean::or_kleene(mask, &nulls).ok()
2708        }
2709        Some(text) => {
2710            if text.len() != mask.len() {
2711                return None;
2712            }
2713            let mut out = arrow_array::builder::BooleanBuilder::with_capacity(mask.len());
2714            for i in 0..mask.len() {
2715                if mask.is_valid(i) {
2716                    out.append_value(mask.value(i));
2717                } else if text.is_valid(i) {
2718                    let equal = crate::locale::Locale::invariant()
2719                        .parse_number_invariant(text.value(i))
2720                        .is_some_and(|x| (x - n).abs() < 1e-12);
2721                    out.append_value(equal != ne);
2722                } else {
2723                    out.append_value(ne);
2724                }
2725            }
2726            Some(out.finish())
2727        }
2728    }
2729}
2730
2731fn compute_criteria_mask(
2732    view: &RangeView<'_>,
2733    col_in_view: usize,
2734    pred: &crate::args::CriteriaPredicate,
2735) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2736    use crate::compute_prelude::{boolean, cmp, concat_arrays};
2737    use arrow::compute::kernels::comparison::{ilike, nilike};
2738    use arrow_array::{
2739        Array as _, ArrayRef, BooleanArray, Float64Array, StringArray, builder::BooleanBuilder,
2740    };
2741
2742    // Helper: apply a numeric predicate to a single Float64Array chunk
2743    fn apply_numeric_pred(
2744        chunk: &Float64Array,
2745        pred: &crate::args::CriteriaPredicate,
2746    ) -> Option<BooleanArray> {
2747        match pred {
2748            crate::args::CriteriaPredicate::Gt(n) => {
2749                cmp::gt(chunk, &Float64Array::new_scalar(*n)).ok()
2750            }
2751            crate::args::CriteriaPredicate::Ge(n) => {
2752                cmp::gt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
2753            }
2754            crate::args::CriteriaPredicate::Lt(n) => {
2755                cmp::lt(chunk, &Float64Array::new_scalar(*n)).ok()
2756            }
2757            crate::args::CriteriaPredicate::Le(n) => {
2758                cmp::lt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
2759            }
2760            crate::args::CriteriaPredicate::Eq(v) => match v {
2761                formualizer_common::LiteralValue::Number(x) => {
2762                    cmp::eq(chunk, &Float64Array::new_scalar(*x)).ok()
2763                }
2764                formualizer_common::LiteralValue::Int(i) => {
2765                    cmp::eq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
2766                }
2767                _ => None,
2768            },
2769            crate::args::CriteriaPredicate::Ne(v) => match v {
2770                formualizer_common::LiteralValue::Number(x) => {
2771                    cmp::neq(chunk, &Float64Array::new_scalar(*x)).ok()
2772                }
2773                formualizer_common::LiteralValue::Int(i) => {
2774                    cmp::neq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
2775                }
2776                _ => None,
2777            },
2778            _ => None,
2779        }
2780    }
2781
2782    // Check if this is a numeric predicate that can be applied per-chunk
2783    let is_numeric_pred = matches!(
2784        pred,
2785        crate::args::CriteriaPredicate::Gt(_)
2786            | crate::args::CriteriaPredicate::Ge(_)
2787            | crate::args::CriteriaPredicate::Lt(_)
2788            | crate::args::CriteriaPredicate::Le(_)
2789            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Number(_))
2790            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Int(_))
2791            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Number(_))
2792            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Int(_))
2793    );
2794
2795    // OPTIMIZED PATH: For numeric predicates, apply per-chunk and concatenate boolean masks.
2796    // This avoids materializing the full numeric column (64-bit per element) and instead
2797    // concatenates boolean masks (1-bit per element) - a 64x memory reduction.
2798    if is_numeric_pred {
2799        let mut bool_parts: Vec<BooleanArray> = Vec::new();
2800        // `=n` / `<>n`: the number lane is null for blank, text, boolean and
2801        // error cells. Blanks, booleans and errors never equal a number;
2802        // numeric text does (`5` matches "5"), as in `criteria_match`.
2803        let number = |v: &formualizer_common::LiteralValue| match v {
2804            formualizer_common::LiteralValue::Number(n) => Some(*n),
2805            formualizer_common::LiteralValue::Int(i) => Some(*i as f64),
2806            _ => None,
2807        };
2808        let equality = match pred {
2809            crate::args::CriteriaPredicate::Eq(v) => number(v).map(|n| (n, false)),
2810            crate::args::CriteriaPredicate::Ne(v) => number(v).map(|n| (n, true)),
2811            _ => None,
2812        };
2813        for res in view.numbers_slices() {
2814            let (rs, rl, cols_seg) = res.ok()?;
2815            if col_in_view < cols_seg.len() {
2816                let chunk = cols_seg[col_in_view].as_ref();
2817                let mut mask = apply_numeric_pred(chunk, pred)?;
2818                if let Some((n, ne)) = equality
2819                    && mask.null_count() > 0
2820                {
2821                    mask = fill_numeric_equality_nulls(view, rs, rl, col_in_view, &mask, n, ne)?;
2822                }
2823                bool_parts.push(mask);
2824            }
2825        }
2826
2827        if bool_parts.is_empty() {
2828            return None;
2829        } else if bool_parts.len() == 1 {
2830            return Some(std::sync::Arc::new(bool_parts.remove(0)));
2831        } else {
2832            // Concatenate boolean masks (much cheaper than concatenating Float64 arrays)
2833            let anys: Vec<&dyn arrow_array::Array> = bool_parts
2834                .iter()
2835                .map(|a| a as &dyn arrow_array::Array)
2836                .collect();
2837            let conc: ArrayRef = concat_arrays(&anys).ok()?;
2838            let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2839            return Some(std::sync::Arc::new(ba));
2840        }
2841    }
2842
2843    // Wildcards and numeric text equality can match non-text cells. The lowered
2844    // base lane is text-only, unlike the scalar matcher. Keep the vectorized
2845    // path for text-only data, but cache a scalar-equivalent mask for mixed data.
2846    if is_numeric_text_equality(pred)
2847        || matches!(pred, crate::args::CriteriaPredicate::TextLike { .. })
2848    {
2849        for tags in view.type_tags_slices() {
2850            let (_, _, cols) = tags.ok()?;
2851            let tags = cols.get(col_in_view)?;
2852            if tags.values().iter().any(|tag| {
2853                *tag == crate::arrow_store::TypeTag::Empty as u8
2854                    || *tag == crate::arrow_store::TypeTag::Number as u8
2855                    || *tag == crate::arrow_store::TypeTag::Boolean as u8
2856            }) {
2857                let mut mask = BooleanBuilder::new();
2858                for chunk in view.iter_row_chunks() {
2859                    let chunk = chunk.ok()?;
2860                    for row in chunk.row_start..chunk.row_start + chunk.row_len {
2861                        mask.append_value(crate::builtins::criteria_match(
2862                            pred,
2863                            &view.get_cell(row, col_in_view),
2864                        ));
2865                    }
2866                }
2867                return Some(std::sync::Arc::new(mask.finish()));
2868            }
2869        }
2870    }
2871
2872    // Keep spreadsheet tilde escapes in the shared scalar matcher.
2873    if matches!(pred, crate::args::CriteriaPredicate::TextLike { pattern, .. }
2874        if pattern.contains('~'))
2875    {
2876        return None;
2877    }
2878
2879    // TEXT PATH: build masks per row-chunk using lowered text slices.
2880    // This avoids concatenating full-string columns just to compute a boolean mask.
2881    // `"="` / `"<>"`: truly blank cells only (empty text is not blank).
2882    if matches!(
2883        pred,
2884        crate::args::CriteriaPredicate::IsBlank | crate::args::CriteriaPredicate::NotBlank
2885    ) {
2886        let want_blank = matches!(pred, crate::args::CriteriaPredicate::IsBlank);
2887        let mut bool_parts: Vec<BooleanArray> = Vec::new();
2888        for tags in view.type_tags_slices() {
2889            let (_, _, cols) = tags.ok()?;
2890            let tags = cols.get(col_in_view)?;
2891            let mut bb = BooleanBuilder::with_capacity(tags.len());
2892            for i in 0..tags.len() {
2893                let blank = tags.value(i) == crate::arrow_store::TypeTag::Empty as u8;
2894                bb.append_value(blank == want_blank);
2895            }
2896            bool_parts.push(bb.finish());
2897        }
2898        return match bool_parts.len() {
2899            0 => None,
2900            1 => Some(std::sync::Arc::new(bool_parts.remove(0))),
2901            _ => {
2902                let anys: Vec<&dyn arrow_array::Array> = bool_parts
2903                    .iter()
2904                    .map(|a| a as &dyn arrow_array::Array)
2905                    .collect();
2906                let conc: ArrayRef = concat_arrays(&anys).ok()?;
2907                let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2908                Some(std::sync::Arc::new(ba))
2909            }
2910        };
2911    }
2912
2913    // Arrow LIKE uses backslash escapes; SQL punctuation is literal in Excel.
2914    let like_pattern = |text: &str, wildcards: bool| {
2915        let mut pattern = String::with_capacity(text.len());
2916        for ch in text.chars() {
2917            match ch {
2918                '%' | '_' | '\\' => {
2919                    pattern.push('\\');
2920                    pattern.push(ch);
2921                }
2922                '*' if wildcards => pattern.push('%'),
2923                '?' if wildcards => pattern.push('_'),
2924                _ => pattern.push(ch),
2925            }
2926        }
2927        pattern
2928    };
2929    let (text_kind, text_pat, empty_special) = match pred {
2930        crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(t)) => {
2931            (0u8, like_pattern(&t.to_lowercase(), false), t.is_empty())
2932        }
2933        crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Text(t)) => {
2934            (1u8, like_pattern(&t.to_lowercase(), false), false)
2935        }
2936        crate::args::CriteriaPredicate::TextLike {
2937            pattern,
2938            case_insensitive,
2939        } => {
2940            let p = if *case_insensitive {
2941                pattern.to_lowercase()
2942            } else {
2943                pattern.clone()
2944            };
2945            (2u8, like_pattern(&p, true), false)
2946        }
2947        _ => return None,
2948    };
2949
2950    let text_pat_is_empty = text_pat.is_empty();
2951    let ne_matches_blank = text_kind == 1 && !text_pat_is_empty;
2952    let pat = StringArray::new_scalar(text_pat);
2953    let mut bool_parts: Vec<BooleanArray> = Vec::new();
2954
2955    let mut tag_slices = view.type_tags_slices();
2956    for res in view.iter_row_chunks() {
2957        let cs = res.ok()?;
2958        if cs.row_len == 0 {
2959            continue;
2960        }
2961        #[cfg(test)]
2962        criteria_mask_test_hooks::inc_total();
2963
2964        let slices = view.slice_lowered_text(cs.row_start, cs.row_len);
2965        if col_in_view >= slices.len() {
2966            return None;
2967        }
2968
2969        let seg_opt = slices[col_in_view].as_ref().map(|a| a.as_ref());
2970        if empty_special || (text_kind == 1 && text_pat_is_empty) {
2971            let (tag_start, tag_len, tags) = tag_slices.next()?.ok()?;
2972            if tag_start != cs.row_start || tag_len != cs.row_len {
2973                return None;
2974            }
2975            let tags = tags.get(col_in_view)?;
2976            // A null text lane is not a blank cell: base numeric/boolean/error
2977            // cells also have null text. Consult the overlay-aware type tags,
2978            // and inspect strings only to distinguish empty text from text.
2979            let strings = seg_opt.and_then(|a| a.as_any().downcast_ref::<StringArray>());
2980            let mut bb = BooleanBuilder::with_capacity(cs.row_len);
2981            for i in 0..cs.row_len {
2982                let blank = tags.value(i) == crate::arrow_store::TypeTag::Empty as u8
2983                    || (tags.value(i) == crate::arrow_store::TypeTag::Text as u8
2984                        && strings.is_some_and(|s| s.is_valid(i) && s.value(i).is_empty()));
2985                bb.append_value(if text_kind == 0 { blank } else { !blank });
2986            }
2987            #[cfg(test)]
2988            if seg_opt.is_none() {
2989                criteria_mask_test_hooks::inc_all_null();
2990            }
2991            bool_parts.push(bb.finish());
2992            continue;
2993        }
2994        let seg = match seg_opt {
2995            Some(s) => s,
2996            None => {
2997                #[cfg(test)]
2998                criteria_mask_test_hooks::inc_all_null();
2999                if (text_kind == 0 && empty_special) || ne_matches_blank {
3000                    // Eq("") treats nulls (Empty) as equal.
3001                    let mut bb = BooleanBuilder::with_capacity(cs.row_len);
3002                    bb.append_n(cs.row_len, true);
3003                    bool_parts.push(bb.finish());
3004                } else {
3005                    // For non-empty patterns, ilike/nilike return null on null inputs.
3006                    bool_parts.push(BooleanArray::new_null(cs.row_len));
3007                }
3008                continue;
3009            }
3010        };
3011
3012        let seg_sa = seg.as_any().downcast_ref::<StringArray>()?;
3013        let mut m = match text_kind {
3014            0 => ilike(seg_sa, &pat).ok()?,
3015            1 => nilike(seg_sa, &pat).ok()?,
3016            2 => ilike(seg_sa, &pat).ok()?,
3017            _ => return None,
3018        };
3019
3020        // Only fold blank/Empty (null) cells into the mask when the segment
3021        // actually contains any. The null-fill loop + or_kleene are pure
3022        // overhead on blank-free chunks, so a `<>text` (or `=""`) aggregation
3023        // over a column with no blanks stays fully vectorized on the ilike/
3024        // nilike result.
3025        if ((text_kind == 0 && empty_special) || ne_matches_blank) && seg_sa.null_count() > 0 {
3026            // Treat nulls as equal to empty string
3027            let mut bb = BooleanBuilder::with_capacity(seg_sa.len());
3028            for i in 0..seg_sa.len() {
3029                bb.append_value(seg_sa.is_null(i));
3030            }
3031            let nulls = bb.finish();
3032            m = boolean::or_kleene(&m, &nulls).ok()?;
3033        }
3034
3035        bool_parts.push(m);
3036    }
3037
3038    if bool_parts.is_empty() {
3039        None
3040    } else if bool_parts.len() == 1 {
3041        Some(std::sync::Arc::new(bool_parts.remove(0)))
3042    } else {
3043        let anys: Vec<&dyn arrow_array::Array> = bool_parts
3044            .iter()
3045            .map(|a| a as &dyn arrow_array::Array)
3046            .collect();
3047        let conc: ArrayRef = concat_arrays(&anys).ok()?;
3048        let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
3049        Some(std::sync::Arc::new(ba))
3050    }
3051}
3052
3053#[derive(Debug, Clone)]
3054pub struct LayerInfo {
3055    pub vertex_count: usize,
3056    pub parallel_eligible: bool,
3057    pub sample_cells: Vec<String>, // Sample of up to 5 cell addresses
3058}
3059
3060#[derive(Debug, Clone)]
3061pub struct EvalPlan {
3062    pub total_vertices_to_evaluate: usize,
3063    pub layers: Vec<LayerInfo>,
3064    pub cycles_detected: usize,
3065    pub dirty_count: usize,
3066    pub volatile_count: usize,
3067    pub parallel_enabled: bool,
3068    pub estimated_parallel_layers: usize,
3069    pub target_cells: Vec<String>,
3070}
3071
3072/// Whether the configured FormulaPlane mode is ignored (spans never placed):
3073/// always, since the dependency authority is the runtime path (design §10).
3074#[inline]
3075fn plane_mode_ignored() -> bool {
3076    true
3077}
3078
3079/// Test-support probe for sibling-crate tests: true when span placement is
3080/// off because the FormulaPlane mode is ignored (see `plane_mode_ignored`).
3081#[cfg(feature = "test-support")]
3082#[doc(hidden)]
3083pub fn formula_plane_mode_ignored_for_test() -> bool {
3084    plane_mode_ignored()
3085}
3086
3087impl<R> Engine<R>
3088where
3089    R: EvaluationContext,
3090{
3091    /// # Panics
3092    /// Panics when `config.cycle` is invalid ([`CycleConfig::validate`],
3093    /// spec §2): `Iterate` with `detection: Static`, `max_iterations == 0`,
3094    /// or a negative/non-finite `max_change`. `EvalConfig::with_cycle`
3095    /// rejects these at build; this re-validates configs assembled via
3096    /// struct literals.
3097    pub fn new(resolver: R, config: EvalConfig) -> Self {
3098        let resolver = crate::guarded_resolver::GuardedResolver::new(resolver);
3099        // Under the unified authority the FormulaPlane mode is accepted and
3100        // ignored (design §10). Normalizing the stored mode keeps external
3101        // readers of `config` (loaders choosing a span-preparation route) on
3102        // the per-cell path; engine reads go through `formula_plane_mode()`.
3103        let config = if plane_mode_ignored() {
3104            EvalConfig {
3105                formula_plane_mode: FormulaPlaneMode::Off,
3106                ..config
3107            }
3108        } else {
3109            config
3110        };
3111        if let Err(msg) = config.cycle.validate() {
3112            panic!("invalid CycleConfig: {msg}");
3113        }
3114        crate::builtins::load_builtins();
3115        let resolved_resources = crate::engine::resource_ledger::resolve_evaluation_budgets(
3116            &config.evaluation_budgets,
3117            config.max_vertices,
3118            config.max_memory_mb,
3119            config.max_eval_time,
3120        );
3121
3122        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
3123            #[cfg(feature = "system-clock")]
3124            {
3125                Arc::new(crate::timezone::SystemClock::new(
3126                    crate::timezone::TimeZoneSpec::default(),
3127                ))
3128            }
3129            #[cfg(not(feature = "system-clock"))]
3130            {
3131                Arc::new(crate::timezone::FixedClock::new(
3132                    chrono::DateTime::UNIX_EPOCH,
3133                    crate::timezone::TimeZoneSpec::Utc,
3134                ))
3135            }
3136        });
3137
3138        // Initialize thread pool based on config
3139        let thread_pool = if config.enable_parallel {
3140            let mut builder = ThreadPoolBuilder::new();
3141            if let Some(max_threads) = config.max_threads {
3142                builder = builder.num_threads(max_threads);
3143            }
3144
3145            match builder.build() {
3146                Ok(pool) => Some(Arc::new(pool)),
3147                Err(_) => {
3148                    // Fall back to sequential evaluation if thread pool creation fails
3149                    None
3150                }
3151            }
3152        } else {
3153            None
3154        };
3155
3156        // C1a retained/cache budgets are observational; cache defaults stay explicit.
3157        let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
3158        let function_provider_revision_seen = resolver.planning_semantic_revision();
3159        let mut engine = Self {
3160            criteria_mask_cache: Default::default(),
3161            graph: DependencyGraph::new_with_config(config.clone()),
3162            resolver,
3163            resolved_functions: Default::default(),
3164            operation_diagnostic_collector: None,
3165            resolved_sheet_names: FxHashMap::default(),
3166            resolution_sheet_revision: None,
3167            resolution_arrow_sheet_names: Vec::new(),
3168            resolved_arrow_sheets: Vec::new(),
3169            arrow_sheet_index: std::sync::OnceLock::new(),
3170            config,
3171            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
3172            clock: crate::timezone::SnapshotClock::new(clock),
3173            thread_pool,
3174            recalc_epoch: 0,
3175            resumable: None,
3176            resumable_call: false,
3177            recalc_options: None,
3178            #[cfg(test)]
3179            resumable_stop_after_units_for_test: None,
3180            snapshot_id: std::sync::atomic::AtomicU64::new(1),
3181            source_mutation_generation: 0,
3182            topology_epoch: 0,
3183            cached_static_schedule: None,
3184            recent_schedules: Vec::new(),
3185            base_schedule: None,
3186            #[cfg(any(test, feature = "benchmark_internal"))]
3187            recalc_reuse_probe: std::sync::Mutex::new(RecalcReuseProbe::default()),
3188            spill_mgr: ShimSpillManager::default(),
3189            arrow_sheets: SheetStore::default(),
3190            format_registry: crate::format::FormatRegistry::default(),
3191            derived_formats: Default::default(),
3192            #[cfg(test)]
3193            derived_format_operations_for_test: std::sync::atomic::AtomicU64::new(0),
3194            #[cfg(test)]
3195            family_members_for_test: std::sync::atomic::AtomicU64::new(0),
3196            #[cfg(test)]
3197            invariant_bound_members_for_test: std::sync::atomic::AtomicU64::new(0),
3198            #[cfg(test)]
3199            lifted_members_for_test: std::sync::atomic::AtomicU64::new(0),
3200            #[cfg(test)]
3201            chained_members_for_test: std::sync::atomic::AtomicU64::new(0),
3202            #[cfg(test)]
3203            lane_clean_reads_for_test: std::sync::atomic::AtomicU64::new(0),
3204            #[cfg(test)]
3205            criteria_kernel_members_for_test: std::sync::atomic::AtomicU64::new(0),
3206            #[cfg(test)]
3207            memo_hits_for_test: std::sync::atomic::AtomicU64::new(0),
3208            compressed_at_build: None,
3209            #[cfg(test)]
3210            computed_overlay_set_explicit_entry_operations_for_test: 0,
3211            #[cfg(test)]
3212            computed_overlay_stale_clear_range_effects_for_test: 0,
3213            #[cfg(test)]
3214            computed_overlay_stale_clear_offset_attempts_for_test: 0,
3215            #[cfg(test)]
3216            computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64::new(0),
3217            has_edited: false,
3218            overlay_compactions: 0,
3219            computed_overlay_bytes_estimate: 0,
3220            computed_overlay_mirroring_disabled: false,
3221            force_materialize_range_views: false,
3222            row_bounds_cache: std::sync::RwLock::new(None),
3223            used_axis_bounds_cache: std::sync::RwLock::new(None),
3224            lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
3225            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
3226            source_formula_token: Arc::new(()),
3227            source_family_mode: crate::engine::SourceFamilyMode::from_env(),
3228            source_family_counters: crate::engine::SourceFamilyCounters::default(),
3229            recalc_plan_token: Arc::new(()),
3230            staged_formulas: std::collections::HashMap::new(),
3231            staged_formula_index: StagedFormulaIndex::default(),
3232            blocked_pending_spills: Vec::new(),
3233            spill_blocker_waiters: SpillBlockerWaiters::default(),
3234            row_visibility: FxHashMap::default(),
3235            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
3236            formula_parse_diagnostics: Vec::new(),
3237            last_formula_ingest_report: None,
3238            formula_ingest_report_total: FormulaIngestReport::default(),
3239            active_cancel_flag: None,
3240            active_evaluation_deadline: None,
3241            action_depth: 0,
3242            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
3243            virtual_dep_fallback_activations: 0,
3244            last_cycle_telemetry: CycleTelemetry::default(),
3245            next_evaluation_resource_request_id: 1,
3246            poisoned: Arc::new(std::sync::atomic::AtomicBool::new(false)),
3247            #[cfg(test)]
3248            panic_internal_once: false,
3249            #[cfg(test)]
3250            panic_kernel_after_member: std::sync::atomic::AtomicUsize::new(usize::MAX),
3251            evaluation_resource_request_depth: 0,
3252            active_evaluation_resource_request: None,
3253            last_evaluation_resource_request: None,
3254            evaluation_resource_baseline: EvaluationResourceBaselineStats::default(),
3255            evaluation_resource_request_started_at: None,
3256            evaluation_resource_budgets: resolved_resources.budgets,
3257            evaluation_resource_config_diagnostic: resolved_resources.diagnostic,
3258            active_resource_ledger: None,
3259            source_cache_footprints: Vec::new(),
3260            source_cache_accounted: 0,
3261            pending_iterative_redirty: Vec::new(),
3262            snapshot_evaluation_active: false,
3263            snapshot_volatile_redirty_pending: false,
3264            snapshot_iterative_redirty_pending: Vec::new(),
3265            retained_scc_members: FxHashMap::default(),
3266            next_retained_scc_id: 0,
3267            retained_scc_config_fingerprint: 0,
3268            retained_scc_function_epoch_seen: 0,
3269            retained_scc_provider_revision_seen: None,
3270            retained_scc_dirty_at_begin: Vec::new(),
3271            iterative_state_values: FxHashMap::default(),
3272            function_semantic_epoch_seen: crate::function_registry::semantic_epoch(),
3273            function_provider_revision_seen,
3274            #[cfg(feature = "tracing")]
3275            trace_evaluation_counters: TraceEvaluationCounters::default(),
3276            #[cfg(test)]
3277            evaluation_request_begin_count_for_test: 0,
3278            #[cfg(any(test, feature = "test-support"))]
3279            before_prepared_span_commit_hook: None,
3280            #[cfg(test)]
3281            before_target_preparation_commit_hook: None,
3282            #[cfg(test)]
3283            before_target_planning_snapshot_hook: None,
3284            #[cfg(test)]
3285            inject_target_semantic_stale_once_for_test: false,
3286            #[cfg(test)]
3287            force_virtual_dep_changes_remaining_for_test: 0,
3288            freshness: Default::default(),
3289            #[cfg(test)]
3290            fail_evaluation_commit_preflight_once_for_test: false,
3291            #[cfg(test)]
3292            target_preparation_fault_for_test: None,
3293            #[cfg(test)]
3294            force_non_cycle_schedule_fallback_for_test: false,
3295            #[cfg(test)]
3296            before_legacy_fallback_final_provider_sample_hook: None,
3297            #[cfg(test)]
3298            after_eager_proposal_commit_hook: None,
3299        };
3300        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
3301        engine.config.arrow_storage_enabled = true;
3302        engine.config.delta_overlay_enabled = true;
3303        engine.config.write_formula_overlay_enabled = true;
3304        let default_sheet = engine.graph.default_sheet_name().to_string();
3305        engine.ensure_arrow_sheet(&default_sheet);
3306        engine
3307    }
3308
3309    /// Create an Engine with a custom thread pool (for shared thread pool scenarios)
3310    ///
3311    /// # Panics
3312    /// Panics when `config.cycle` is invalid, exactly like [`Engine::new`].
3313    pub fn with_thread_pool(
3314        resolver: R,
3315        config: EvalConfig,
3316        thread_pool: Arc<rayon::ThreadPool>,
3317    ) -> Self {
3318        let resolver = crate::guarded_resolver::GuardedResolver::new(resolver);
3319        if let Err(msg) = config.cycle.validate() {
3320            panic!("invalid CycleConfig: {msg}");
3321        }
3322        crate::builtins::load_builtins();
3323        let resolved_resources = crate::engine::resource_ledger::resolve_evaluation_budgets(
3324            &config.evaluation_budgets,
3325            config.max_vertices,
3326            config.max_memory_mb,
3327            config.max_eval_time,
3328        );
3329        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
3330            #[cfg(feature = "system-clock")]
3331            {
3332                Arc::new(crate::timezone::SystemClock::new(
3333                    crate::timezone::TimeZoneSpec::default(),
3334                ))
3335            }
3336            #[cfg(not(feature = "system-clock"))]
3337            {
3338                Arc::new(crate::timezone::FixedClock::new(
3339                    chrono::DateTime::UNIX_EPOCH,
3340                    crate::timezone::TimeZoneSpec::Utc,
3341                ))
3342            }
3343        });
3344        // C1a retained/cache budgets are observational; cache defaults stay explicit.
3345        let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
3346        let function_provider_revision_seen = resolver.planning_semantic_revision();
3347        let mut engine = Self {
3348            criteria_mask_cache: Default::default(),
3349            graph: DependencyGraph::new_with_config(config.clone()),
3350            resolver,
3351            resolved_functions: Default::default(),
3352            operation_diagnostic_collector: None,
3353            resolved_sheet_names: FxHashMap::default(),
3354            resolution_sheet_revision: None,
3355            resolution_arrow_sheet_names: Vec::new(),
3356            resolved_arrow_sheets: Vec::new(),
3357            arrow_sheet_index: std::sync::OnceLock::new(),
3358            config,
3359            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
3360            clock: crate::timezone::SnapshotClock::new(clock),
3361            thread_pool: Some(thread_pool),
3362            recalc_epoch: 0,
3363            resumable: None,
3364            resumable_call: false,
3365            recalc_options: None,
3366            #[cfg(test)]
3367            resumable_stop_after_units_for_test: None,
3368            snapshot_id: std::sync::atomic::AtomicU64::new(1),
3369            source_mutation_generation: 0,
3370            topology_epoch: 0,
3371            cached_static_schedule: None,
3372            recent_schedules: Vec::new(),
3373            base_schedule: None,
3374            #[cfg(any(test, feature = "benchmark_internal"))]
3375            recalc_reuse_probe: std::sync::Mutex::new(RecalcReuseProbe::default()),
3376            spill_mgr: ShimSpillManager::default(),
3377            arrow_sheets: SheetStore::default(),
3378            format_registry: crate::format::FormatRegistry::default(),
3379            derived_formats: Default::default(),
3380            #[cfg(test)]
3381            derived_format_operations_for_test: std::sync::atomic::AtomicU64::new(0),
3382            #[cfg(test)]
3383            family_members_for_test: std::sync::atomic::AtomicU64::new(0),
3384            #[cfg(test)]
3385            invariant_bound_members_for_test: std::sync::atomic::AtomicU64::new(0),
3386            #[cfg(test)]
3387            lifted_members_for_test: std::sync::atomic::AtomicU64::new(0),
3388            #[cfg(test)]
3389            chained_members_for_test: std::sync::atomic::AtomicU64::new(0),
3390            #[cfg(test)]
3391            lane_clean_reads_for_test: std::sync::atomic::AtomicU64::new(0),
3392            #[cfg(test)]
3393            criteria_kernel_members_for_test: std::sync::atomic::AtomicU64::new(0),
3394            #[cfg(test)]
3395            memo_hits_for_test: std::sync::atomic::AtomicU64::new(0),
3396            compressed_at_build: None,
3397            #[cfg(test)]
3398            computed_overlay_set_explicit_entry_operations_for_test: 0,
3399            #[cfg(test)]
3400            computed_overlay_stale_clear_range_effects_for_test: 0,
3401            #[cfg(test)]
3402            computed_overlay_stale_clear_offset_attempts_for_test: 0,
3403            #[cfg(test)]
3404            computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64::new(0),
3405            has_edited: false,
3406            overlay_compactions: 0,
3407            computed_overlay_bytes_estimate: 0,
3408            computed_overlay_mirroring_disabled: false,
3409            force_materialize_range_views: false,
3410            row_bounds_cache: std::sync::RwLock::new(None),
3411            used_axis_bounds_cache: std::sync::RwLock::new(None),
3412            lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
3413            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
3414            source_formula_token: Arc::new(()),
3415            source_family_mode: crate::engine::SourceFamilyMode::from_env(),
3416            source_family_counters: crate::engine::SourceFamilyCounters::default(),
3417            recalc_plan_token: Arc::new(()),
3418            staged_formulas: std::collections::HashMap::new(),
3419            staged_formula_index: StagedFormulaIndex::default(),
3420            blocked_pending_spills: Vec::new(),
3421            spill_blocker_waiters: SpillBlockerWaiters::default(),
3422            row_visibility: FxHashMap::default(),
3423            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
3424            formula_parse_diagnostics: Vec::new(),
3425            last_formula_ingest_report: None,
3426            formula_ingest_report_total: FormulaIngestReport::default(),
3427            active_cancel_flag: None,
3428            active_evaluation_deadline: None,
3429            action_depth: 0,
3430            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
3431            virtual_dep_fallback_activations: 0,
3432            last_cycle_telemetry: CycleTelemetry::default(),
3433            next_evaluation_resource_request_id: 1,
3434            poisoned: Arc::new(std::sync::atomic::AtomicBool::new(false)),
3435            #[cfg(test)]
3436            panic_internal_once: false,
3437            #[cfg(test)]
3438            panic_kernel_after_member: std::sync::atomic::AtomicUsize::new(usize::MAX),
3439            evaluation_resource_request_depth: 0,
3440            active_evaluation_resource_request: None,
3441            last_evaluation_resource_request: None,
3442            evaluation_resource_baseline: EvaluationResourceBaselineStats::default(),
3443            evaluation_resource_request_started_at: None,
3444            evaluation_resource_budgets: resolved_resources.budgets,
3445            evaluation_resource_config_diagnostic: resolved_resources.diagnostic,
3446            active_resource_ledger: None,
3447            source_cache_footprints: Vec::new(),
3448            source_cache_accounted: 0,
3449            pending_iterative_redirty: Vec::new(),
3450            snapshot_evaluation_active: false,
3451            snapshot_volatile_redirty_pending: false,
3452            snapshot_iterative_redirty_pending: Vec::new(),
3453            retained_scc_members: FxHashMap::default(),
3454            next_retained_scc_id: 0,
3455            retained_scc_config_fingerprint: 0,
3456            retained_scc_function_epoch_seen: 0,
3457            retained_scc_provider_revision_seen: None,
3458            retained_scc_dirty_at_begin: Vec::new(),
3459            iterative_state_values: FxHashMap::default(),
3460            function_semantic_epoch_seen: crate::function_registry::semantic_epoch(),
3461            function_provider_revision_seen,
3462            #[cfg(feature = "tracing")]
3463            trace_evaluation_counters: TraceEvaluationCounters::default(),
3464            #[cfg(test)]
3465            evaluation_request_begin_count_for_test: 0,
3466            #[cfg(any(test, feature = "test-support"))]
3467            before_prepared_span_commit_hook: None,
3468            #[cfg(test)]
3469            before_target_preparation_commit_hook: None,
3470            #[cfg(test)]
3471            before_target_planning_snapshot_hook: None,
3472            #[cfg(test)]
3473            inject_target_semantic_stale_once_for_test: false,
3474            #[cfg(test)]
3475            force_virtual_dep_changes_remaining_for_test: 0,
3476            freshness: Default::default(),
3477            #[cfg(test)]
3478            fail_evaluation_commit_preflight_once_for_test: false,
3479            #[cfg(test)]
3480            target_preparation_fault_for_test: None,
3481            #[cfg(test)]
3482            force_non_cycle_schedule_fallback_for_test: false,
3483            #[cfg(test)]
3484            before_legacy_fallback_final_provider_sample_hook: None,
3485            #[cfg(test)]
3486            after_eager_proposal_commit_hook: None,
3487        };
3488        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
3489        engine.config.arrow_storage_enabled = true;
3490        engine.config.delta_overlay_enabled = true;
3491        engine.config.write_formula_overlay_enabled = true;
3492        let default_sheet = engine.graph.default_sheet_name().to_string();
3493        engine.ensure_arrow_sheet(&default_sheet);
3494        engine
3495    }
3496
3497    pub fn workbook_load_limits(&self) -> &crate::engine::WorkbookLoadLimits {
3498        &self.workbook_load_limits
3499    }
3500
3501    ///
3502    /// # Panics
3503    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
3504    pub fn set_workbook_load_limits(&mut self, limits: crate::engine::WorkbookLoadLimits) {
3505        self.assert_not_poisoned();
3506        self.workbook_load_limits = limits;
3507    }
3508
3509    fn clear_source_cache(&self) {
3510        if let Ok(mut g) = self.source_cache.write() {
3511            *g = SourceCache::default();
3512        }
3513    }
3514
3515    pub fn last_virtual_dep_telemetry(&self) -> &VirtualDepTelemetry {
3516        &self.last_virtual_dep_telemetry
3517    }
3518
3519    /// Telemetry from Runtime SCC evaluation during the most recent
3520    /// evaluation request (always default-zero under `CycleDetection::Static`
3521    /// or when `enable_virtual_dep_telemetry` is off).
3522    pub fn last_cycle_telemetry(&self) -> &CycleTelemetry {
3523        &self.last_cycle_telemetry
3524    }
3525
3526    /// Resource observations for the most recently completed public evaluation request.
3527    pub fn last_evaluation_resource_request_stats(
3528        &self,
3529    ) -> Option<&EvaluationResourceRequestStats> {
3530        self.last_evaluation_resource_request.as_ref()
3531    }
3532
3533    /// Cumulative resource observations since engine creation or the last telemetry reset.
3534    pub fn evaluation_resource_baseline_stats(&self) -> EvaluationResourceBaselineStats {
3535        self.evaluation_resource_baseline
3536    }
3537
3538    pub fn evaluation_resource_budgets(&self) -> &crate::engine::EvaluationBudgets {
3539        &self.evaluation_resource_budgets
3540    }
3541
3542    /// At most one diagnostic is emitted for deprecated resource fields.
3543    pub fn evaluation_resource_config_diagnostic(
3544        &self,
3545    ) -> Option<&crate::engine::EvaluationResourceConfigDiagnostic> {
3546        self.evaluation_resource_config_diagnostic.as_ref()
3547    }
3548
3549    /// Reset accumulated and last-request observations without reusing request IDs.
3550    ///
3551    /// # Panics
3552    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
3553    pub fn reset_evaluation_resource_telemetry(&mut self) {
3554        self.assert_not_poisoned();
3555        self.evaluation_resource_baseline = EvaluationResourceBaselineStats::default();
3556        self.last_evaluation_resource_request = None;
3557    }
3558
3559    // Reconcile without replay locks: several packages may share the same Arc.
3560    // Weak tokens neither retain dead packages nor duplicate their allocations.
3561    fn reconcile_source_cache_footprints(&mut self) -> Result<(), ExcelError> {
3562        self.blocked_pending_spills.retain(|&(vertex, anchor, _)| {
3563            self.graph.vertex_exists(vertex)
3564                && self.graph.get_cell_ref(vertex) == Some(anchor)
3565                && matches!(
3566                    self.graph.get_vertex_kind(vertex),
3567                    VertexKind::FormulaScalar | VertexKind::FormulaArray
3568                )
3569        });
3570        if self.blocked_pending_spills.is_empty() {
3571            self.blocked_pending_spills = Vec::new();
3572        }
3573        let mut bytes = (self.blocked_pending_spills.capacity()
3574            * std::mem::size_of::<(VertexId, CellRef, Region)>()) as u64;
3575        self.source_cache_footprints.retain(|weak| {
3576            let Some(footprint) = weak.upgrade() else {
3577                return false;
3578            };
3579            bytes = bytes.saturating_add(footprint.load(std::sync::atomic::Ordering::Acquire));
3580            true
3581        });
3582        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3583            ledger
3584                .release_retained(self.source_cache_accounted)
3585                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3586            // Observe first: even a tightened budget cannot erase live ownership.
3587            ledger.observe_retained(bytes);
3588            self.source_cache_accounted = bytes;
3589            ledger
3590                .reserve_retained(0)
3591                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3592        }
3593        Ok(())
3594    }
3595
3596    #[cfg(test)]
3597    pub(crate) fn request_scope_for_test(&self) -> (usize, bool, bool) {
3598        (
3599            self.evaluation_resource_request_depth,
3600            self.resumable_call,
3601            self.active_cancel_flag.is_some(),
3602        )
3603    }
3604
3605    fn duration_ns(duration: std::time::Duration) -> u64 {
3606        u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
3607    }
3608
3609    fn observe_evaluation_resource_request<T>(
3610        &mut self,
3611        kind: EvaluationRequestKind,
3612        evaluate: impl FnOnce(&mut Self) -> Result<T, ExcelError>,
3613    ) -> Result<T, ExcelError> {
3614        self.ensure_not_poisoned()?;
3615        let outermost = self.evaluation_resource_request_depth == 0;
3616        let _unwind_poison =
3617            outermost.then(|| super::poison::UnwindPoisonGuard(self.poisoned.clone()));
3618        if outermost && !self.resumable_call && self.has_active_recalc_job() {
3619            return Err(Self::active_recalc_job_error());
3620        }
3621        // Foreign requests settle the logical recalculation before any early
3622        // admission/cancellation exit. Every resumable pass is already settled.
3623        if outermost && !self.resumable_call && self.resumable.take().is_some() {
3624            self.redirty_for_next_recalc();
3625            self.freshness_begin_request();
3626        }
3627        // Resumable calls have per-call resource ledgers, but one collector.
3628        let continuing = self.resumable_call && self.resumable.is_some();
3629        if outermost {
3630            self.resolver
3631                .callbacks
3632                .begin(self.operation_diagnostic_collector.as_ref(), !continuing);
3633        }
3634        let _callbacks = crate::external_callbacks::enter(self.resolver.callbacks.clone());
3635        // Request evidence starts here, before cancellation/admission can exit.
3636        // Nested entry points and internal replans retain the outer union.
3637        if outermost
3638            && !continuing
3639            && let Some(collector) = &self.operation_diagnostic_collector
3640        {
3641            collector.reset();
3642        }
3643        if outermost {
3644            let pending = std::mem::take(&mut self.snapshot_iterative_redirty_pending);
3645            if !pending.is_empty() {
3646                self.graph.redirty_iterative_members(&pending);
3647            }
3648        }
3649        if outermost && self.snapshot_volatile_redirty_pending {
3650            self.snapshot_volatile_redirty_pending = false;
3651            self.graph.redirty_volatiles();
3652        }
3653        #[cfg(feature = "tracing")]
3654        if outermost {
3655            self.trace_evaluation_counters = TraceEvaluationCounters::default();
3656        }
3657        #[cfg(feature = "tracing")]
3658        let request_kind = if matches!(
3659            kind,
3660            EvaluationRequestKind::Full
3661                | EvaluationRequestKind::FullWithDelta
3662                | EvaluationRequestKind::FullCancellable
3663                | EvaluationRequestKind::FullLogged
3664        ) {
3665            "full"
3666        } else {
3667            "targeted"
3668        };
3669        #[cfg(feature = "tracing")]
3670        let _request_span = outermost.then(|| {
3671            crate::engine::trace::fz_span!(
3672                tracing::Level::INFO,
3673                "evaluate",
3674                "evaluate.request",
3675                kind = request_kind,
3676                mode = ?FormulaPlaneMode::Off
3677            )
3678        });
3679        if outermost {
3680            let request_id = self.next_evaluation_resource_request_id;
3681            self.next_evaluation_resource_request_id = request_id
3682                .checked_add(1)
3683                .expect("evaluation resource request ID exhausted");
3684            self.active_evaluation_resource_request = Some(EvaluationResourceRequestStats::new(
3685                request_id,
3686                kind,
3687                FormulaPlaneMode::Off,
3688                self.staged_formula_count(),
3689            ));
3690            self.evaluation_resource_baseline.record_started(request_id);
3691            self.evaluation_resource_request_started_at = Some(crate::instant::FzInstant::now());
3692            self.source_cache_accounted = 0;
3693            let mut ledger =
3694                ResourceLedger::new(Some(request_id), self.evaluation_resource_budgets.clone());
3695            if self.resumable_call
3696                && let Some(state) = &self.resumable
3697            {
3698                ledger.resume_job(state.work_units, state.active_elapsed);
3699            }
3700            self.active_resource_ledger = Some(ledger);
3701        }
3702        self.evaluation_resource_request_depth =
3703            self.evaluation_resource_request_depth.saturating_add(1);
3704        #[cfg(test)]
3705        if outermost && std::mem::take(&mut self.panic_internal_once) {
3706            panic!("injected internal evaluation panic");
3707        }
3708        let result = if outermost {
3709            self.reconcile_source_cache_footprints()
3710                .and_then(|()| self.resource_checkpoint(0))
3711                .and_then(|()| evaluate(self))
3712        } else {
3713            evaluate(self)
3714        };
3715        let result = if let Some(error) = crate::external_callbacks::pending() {
3716            Err(error)
3717        } else {
3718            result
3719        };
3720        if outermost && result.is_err() {
3721            self.freshness_abort_pass();
3722        }
3723        self.evaluation_resource_request_depth =
3724            self.evaluation_resource_request_depth.saturating_sub(1);
3725
3726        let reconciliation = if outermost {
3727            self.reconcile_source_cache_footprints()
3728        } else {
3729            Ok(())
3730        };
3731        let result = result.and_then(|value| reconciliation.map(|()| value));
3732        if outermost && result.is_err() {
3733            self.freshness_abort_pass();
3734            self.freshness_discard_read_evidence();
3735        }
3736        if outermost {
3737            let total_ns = self
3738                .evaluation_resource_request_started_at
3739                .take()
3740                .map(|start| Self::duration_ns(start.elapsed()))
3741                .unwrap_or(0);
3742            let mut stats = self
3743                .active_evaluation_resource_request
3744                .take()
3745                .expect("outer evaluation resource request has active stats");
3746            let mut ledger = self
3747                .active_resource_ledger
3748                .take()
3749                .expect("outer evaluation resource request has active ledger");
3750            ledger.release_all_scratch();
3751            if self.resumable_call
3752                && let Some(state) = &mut self.resumable
3753            {
3754                state.work_units = ledger.snapshot().work_charged;
3755                state.active_elapsed += std::time::Duration::from_nanos(total_ns);
3756            }
3757            stats.ledger.update(ledger.snapshot());
3758            stats.outcome = match &result {
3759                Ok(_) => EvaluationRequestOutcome::Success,
3760                Err(error) if error.kind == ExcelErrorKind::Cancelled => {
3761                    EvaluationRequestOutcome::Cancelled
3762                }
3763                Err(_) => EvaluationRequestOutcome::Error,
3764            };
3765            if stats.dirty_lease == FormulaDirtyLeaseOutcome::Acquired {
3766                stats.dirty_lease = if stats.outcome == EvaluationRequestOutcome::Cancelled {
3767                    FormulaDirtyLeaseOutcome::RetainedOnCancellation
3768                } else {
3769                    FormulaDirtyLeaseOutcome::RetainedOnError
3770                };
3771            }
3772            stats.phases.total_ns = total_ns;
3773            let attributed = stats
3774                .phases
3775                .staged_prepare_ns
3776                .saturating_add(stats.phases.topology_ns)
3777                .saturating_add(stats.phases.materialization_ns);
3778            stats.phases.evaluation_ns = total_ns.saturating_sub(attributed);
3779            self.evaluation_resource_baseline.record_finished(&stats);
3780            self.last_evaluation_resource_request = Some(stats);
3781            crate::engine::trace::fz_event!(
3782                tracing::Level::INFO,
3783                "evaluate",
3784                "evaluate.summary",
3785                computed_vertices = self.trace_evaluation_counters.computed_vertices,
3786                cycles = self.trace_evaluation_counters.cycles,
3787                cancelled = matches!(
3788                    &result,
3789                    Err(error) if error.kind == ExcelErrorKind::Cancelled
3790                )
3791            );
3792        }
3793        if self.resolver.callbacks.failed() && self.resumable.is_none() {
3794            self.resolved_functions = Default::default();
3795        }
3796        result
3797    }
3798
3799    ///
3800    /// # Panics
3801    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
3802    pub fn set_evaluation_resource_budgets(&mut self, budgets: crate::engine::EvaluationBudgets) {
3803        self.assert_not_poisoned();
3804        self.evaluation_resource_budgets = budgets.clone();
3805        self.config.evaluation_budgets = budgets.clone();
3806        self.graph.set_evaluation_budgets(budgets);
3807    }
3808
3809    #[cfg(test)]
3810    pub(crate) fn set_evaluation_budgets_for_test(
3811        &mut self,
3812        budgets: crate::engine::EvaluationBudgets,
3813    ) {
3814        self.set_evaluation_resource_budgets(budgets);
3815    }
3816
3817    fn preflight_evaluation_commit_window(
3818        &mut self,
3819        bounded_writes: usize,
3820    ) -> Result<crate::instant::FzInstant, ExcelError> {
3821        #[cfg(test)]
3822        if std::mem::take(&mut self.fail_evaluation_commit_preflight_once_for_test) {
3823            return Err(crate::engine::ResourceLedgerError::Exhausted(
3824                formualizer_common::ResourceExhaustionDetail {
3825                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
3826                    limit: 0,
3827                    observed: 1,
3828                    request_id: self
3829                        .active_evaluation_resource_request
3830                        .as_ref()
3831                        .map(|stats| stats.request_id),
3832                },
3833            )
3834            .into_excel_error());
3835        }
3836        let estimate = std::time::Duration::from_nanos(
3837            u64::try_from(bounded_writes)
3838                .unwrap_or(u64::MAX)
3839                .saturating_mul(100),
3840        );
3841        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3842            ledger
3843                .preflight_commit_window(estimate)
3844                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3845        }
3846        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3847            stats.evaluation_commit_preflight_count =
3848                stats.evaluation_commit_preflight_count.saturating_add(1);
3849            stats.evaluation_commit_estimated_ns = stats
3850                .evaluation_commit_estimated_ns
3851                .saturating_add(Self::duration_ns(estimate));
3852        }
3853        Ok(crate::instant::FzInstant::now())
3854    }
3855
3856    fn observe_evaluation_commit_window(&mut self, started: crate::instant::FzInstant) {
3857        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3858            stats.evaluation_commit_actual_ns = stats
3859                .evaluation_commit_actual_ns
3860                .saturating_add(Self::duration_ns(started.elapsed()));
3861        }
3862    }
3863
3864    /// Post-work cancellation boundary: call after evaluating a unit (or a
3865    /// parallel group) and before committing it. A function that observed
3866    /// the request's token returns `Err(Cancelled)`, which the evaluator
3867    /// turns into a `#CANCELLED` value; any other result computed across the
3868    /// signal is equally not a finished result. Neither may publish: the
3869    /// caller returns this error before committing, so the unit stays dirty,
3870    /// the request reports `Cancelled`, and a failed pass restores the
3871    /// vertices it already committed (`freshness_abort_pass`). The token is
3872    /// the discriminator: a `#CANCELLED` value with no live cancellation is
3873    /// ordinary data and commits. Deadlines are not checked here; finished
3874    /// work is not discarded on a deadline.
3875    fn live_cancellation_after_work(&self, message: &'static str) -> Result<(), ExcelError> {
3876        if self
3877            .active_cancel_flag
3878            .as_ref()
3879            .is_some_and(|cancel| cancel.is_cancelled())
3880        {
3881            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(message));
3882        }
3883        Ok(())
3884    }
3885
3886    fn cancellation_checkpoint(&self, message: &'static str) -> Result<(), ExcelError> {
3887        if self
3888            .active_cancel_flag
3889            .as_ref()
3890            .is_some_and(|cancel| cancel.is_cancelled())
3891        {
3892            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(message));
3893        }
3894        if self
3895            .active_evaluation_deadline
3896            .is_some_and(|deadline| Instant::now() >= deadline)
3897        {
3898            return Err(crate::engine::ResourceLedgerError::Exhausted(
3899                formualizer_common::ResourceExhaustionDetail {
3900                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
3901                    limit: 0,
3902                    observed: 1,
3903                    request_id: self
3904                        .active_evaluation_resource_request
3905                        .as_ref()
3906                        .map(|request| request.request_id),
3907                },
3908            )
3909            .into_excel_error()
3910            .with_message(message));
3911        }
3912        Ok(())
3913    }
3914
3915    fn resource_checkpoint(&mut self, work_units: u64) -> Result<(), ExcelError> {
3916        let Some(ledger) = self.active_resource_ledger.as_mut() else {
3917            return Ok(());
3918        };
3919        ledger
3920            .charge_work(work_units)
3921            .and_then(|()| ledger.checkpoint_deadline())
3922            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3923    }
3924
3925    fn charge_bounded_work(&mut self, mut work_units: u64) -> Result<(), ExcelError> {
3926        if work_units == 0 {
3927            return self.resource_checkpoint(0);
3928        }
3929        while work_units > 0 {
3930            let chunk = work_units.min(256);
3931            self.resource_checkpoint(chunk)?;
3932            work_units -= chunk;
3933        }
3934        Ok(())
3935    }
3936
3937    fn reserve_request_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3938        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3939            // Exact request topology and activation of the scratch cap are C1b. C1a records
3940            // scoped ownership but must not introduce a new skip or terminal path.
3941            ledger.observe_scratch(bytes);
3942        }
3943        Ok(())
3944    }
3945
3946    fn release_request_scratch(&mut self, bytes: u64) {
3947        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3948            let released = ledger.release_scratch(bytes);
3949            debug_assert!(
3950                released.is_ok(),
3951                "request scratch release exceeded the outstanding reservation"
3952            );
3953        }
3954    }
3955
3956    fn reserve_topology_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3957        self.active_resource_ledger
3958            .as_mut()
3959            .map_or(Ok(()), |ledger| ledger.reserve_schedule_discovery(bytes))
3960            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3961    }
3962
3963    fn reserve_graph_source_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3964        self.active_resource_ledger
3965            .as_mut()
3966            .map_or(Ok(()), |ledger| ledger.reserve_graph_source(bytes))
3967            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3968    }
3969
3970    fn with_request_scratch<T>(
3971        &mut self,
3972        bytes: u64,
3973        work: impl FnOnce(&mut Self) -> Result<T, ExcelError>,
3974    ) -> Result<T, ExcelError> {
3975        self.reserve_request_scratch(bytes)?;
3976        let result = work(self);
3977        self.release_request_scratch(bytes);
3978        result
3979    }
3980
3981    fn observe_staged_preparation(
3982        &mut self,
3983        selected: usize,
3984        retained: usize,
3985        elapsed: std::time::Duration,
3986    ) {
3987        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3988            stats.staged_selected = stats.staged_selected.saturating_add(selected as u64);
3989            stats.staged_retained = retained as u64;
3990            stats.phases.staged_prepare_ns = stats
3991                .phases
3992                .staged_prepare_ns
3993                .saturating_add(Self::duration_ns(elapsed));
3994        }
3995    }
3996
3997    fn graph_admission_enabled(&self) -> bool {
3998        crate::engine::resource_ledger::graph_admission_enabled(&self.evaluation_resource_budgets)
3999    }
4000
4001    fn preflight_graph_admission(
4002        &mut self,
4003        usage: crate::engine::resource_ledger::GraphAdmission,
4004    ) -> Result<(), ExcelError> {
4005        let request_id = self
4006            .active_evaluation_resource_request
4007            .as_ref()
4008            .map(|stats| stats.request_id);
4009        crate::engine::resource_ledger::preflight_graph_admission(
4010            &self.evaluation_resource_budgets,
4011            usage,
4012            request_id,
4013        )
4014        .map_err(crate::engine::ResourceLedgerError::into_excel_error)
4015    }
4016
4017    fn prepared_legacy_admission(
4018        &mut self,
4019        plan: &PreparedLegacyGraphPlan,
4020        materialization_cells: u64,
4021    ) -> Result<(), ExcelError> {
4022        if !self.graph_admission_enabled() {
4023            return Ok(());
4024        }
4025        let stats = self.graph.baseline_stats();
4026        let added_edges = plan.planned_edge_count().ok_or_else(|| {
4027            ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
4028        })?;
4029        let removed_edges = plan.removed_edge_count().ok_or_else(|| {
4030            ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
4031        })?;
4032        self.preflight_graph_admission(crate::engine::resource_ledger::GraphAdmission {
4033            final_vertices: stats
4034                .graph_vertex_count
4035                .checked_add(plan.new_vertex_count())
4036                .ok_or_else(|| {
4037                    ExcelError::new(ExcelErrorKind::NImpl)
4038                        .with_message("graph vertex count overflow")
4039                })?,
4040            final_edges: stats
4041                .graph_edge_count
4042                .checked_sub(removed_edges)
4043                .and_then(|count| count.checked_add(added_edges))
4044                .ok_or_else(|| {
4045                    ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
4046                })?,
4047            materialization_cells,
4048            added_vertices: plan.new_vertex_count(),
4049            added_edges,
4050        })
4051    }
4052
4053    fn observe_target_admission_failure(
4054        &mut self,
4055        reason: formualizer_common::ResourceExhaustionReason,
4056    ) {
4057        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
4058            stats.target_admission_failure = Some(reason);
4059        }
4060    }
4061
4062    fn observe_target_preparation_report(
4063        &mut self,
4064        report: &crate::engine::PreparedTargetGraphReport,
4065    ) {
4066        let reason_bit = |reason: crate::engine::OpaqueReason| -> u64 {
4067            let index = match reason {
4068                crate::engine::OpaqueReason::DynamicReference => 0,
4069                crate::engine::OpaqueReason::RuntimeTextReference => 1,
4070                crate::engine::OpaqueReason::UnknownFunction => 2,
4071                crate::engine::OpaqueReason::UnknownCustomFunction => 3,
4072                crate::engine::OpaqueReason::UnresolvedCrossSheetBinding => 4,
4073                crate::engine::OpaqueReason::UnresolvedName => 5,
4074                crate::engine::OpaqueReason::UnresolvedTable => 6,
4075                crate::engine::OpaqueReason::FormulaName => 7,
4076                crate::engine::OpaqueReason::DeferredSourcePackage => 8,
4077                crate::engine::OpaqueReason::UnsupportedSourceSemantics => 9,
4078                crate::engine::OpaqueReason::UncertainDefaultSheetBinding => 10,
4079            };
4080            1u64 << index
4081        };
4082        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
4083            stats.staged_selected = report.selected_staged_cells as u64;
4084            stats.staged_retained = report.retained_staged_cells as u64;
4085            stats.target_requested = report.requested_targets as u64;
4086            stats.target_normalized_regions = report.normalized_regions as u64;
4087            stats.target_scope_level = match &report.widened_scope {
4088                crate::engine::PrepareScope::Exact => 0,
4089                crate::engine::PrepareScope::Sheets(_) => 1,
4090                crate::engine::PrepareScope::Workbook => 2,
4091            };
4092            stats.target_widening_reason_bits = report
4093                .widening_reasons
4094                .iter()
4095                .copied()
4096                .fold(0, |bits, reason| bits | reason_bit(reason));
4097            stats.graph_source_scratch_estimated = report.estimated_scratch_bytes;
4098            stats.graph_source_scratch_observed = report.observed_scratch_bytes;
4099            stats.target_commit_estimated_work = report.estimated_commit_work;
4100            stats.target_commit_actual_work = report.actual_commit_work;
4101            stats.target_commit_window_ns = Self::duration_ns(report.commit_window);
4102            stats.phases.staged_prepare_ns = stats
4103                .phases
4104                .staged_prepare_ns
4105                .saturating_add(Self::duration_ns(report.commit_window));
4106        }
4107    }
4108
4109    /// Begin a new evaluation request: reset per-recalc cycle telemetry and
4110    /// take the per-recalc volatile clock sample. Called at the start of
4111    /// every evaluation request that walks schedule units.
4112    /// The per-request function table when it describes the current registry,
4113    /// provider and arena; `None` outside a request when it may be stale.
4114    pub(crate) fn current_resolved_functions(
4115        &self,
4116    ) -> Option<&crate::interpreter::ResolvedFunctions> {
4117        self.resolved_functions
4118            .is_current(self.graph.data_store(), &self.resolver)
4119            .then_some(&self.resolved_functions)
4120    }
4121
4122    fn refresh_resolution_snapshots(&mut self) {
4123        if self.resolver.callbacks.failed() {
4124            self.resolved_functions = Default::default();
4125        }
4126        self.resolved_functions
4127            .refresh(self.graph.data_store(), &self.resolver);
4128        let named_asts = self
4129            .graph
4130            .named_ranges_iter()
4131            .map(|(_, named)| named)
4132            .chain(self.graph.sheet_named_ranges_iter().map(|(_, named)| named))
4133            .filter_map(|named| match &named.definition {
4134                NamedDefinition::Formula { ast, .. } => Some(ast),
4135                _ => None,
4136            });
4137        self.resolved_functions.refresh_ast_functions(
4138            self.graph.data_store(),
4139            &self.resolver,
4140            named_asts,
4141            self.graph.symbol_revision(),
4142        );
4143        // Arrow sheet storage is publicly mutable. Compare names and order
4144        // exactly (including same-length replacements) without allocating on
4145        // unchanged requests; a fingerprint alone could silently collide.
4146        let revision = (
4147            self.graph.sheet_reg().revision(),
4148            self.graph.data_store().resolution_revision(),
4149            self.graph.data_store().sheet_names().count(),
4150        );
4151        let arrow_names_unchanged = self
4152            .resolution_arrow_sheet_names
4153            .iter()
4154            .map(|name| name.as_ref())
4155            .eq(self
4156                .arrow_sheets
4157                .sheets
4158                .iter()
4159                .map(|sheet| sheet.name.as_ref()));
4160        if self.resolution_sheet_revision == Some(revision) && arrow_names_unchanged {
4161            return;
4162        }
4163        self.resolution_arrow_sheet_names.clear();
4164        self.resolution_arrow_sheet_names.extend(
4165            self.arrow_sheets
4166                .sheets
4167                .iter()
4168                .map(|sheet| Arc::clone(&sheet.name)),
4169        );
4170        self.resolved_sheet_names.clear();
4171        for id in self.graph.data_store().sheet_names() {
4172            let name = self.graph.data_store().resolve_ast_string(id);
4173            if let Some(sheet_id) = self.graph.sheet_id(name) {
4174                self.resolved_sheet_names.insert(
4175                    id,
4176                    (
4177                        sheet_id,
4178                        self.arrow_sheets
4179                            .sheets
4180                            .iter()
4181                            .position(|sheet| sheet.name.as_ref() == name),
4182                    ),
4183                );
4184            }
4185        }
4186        self.resolved_arrow_sheets.clear();
4187        for (index, sheet) in self.arrow_sheets.sheets.iter().enumerate() {
4188            if let Some(id) = self.graph.sheet_id(&sheet.name) {
4189                let len = usize::from(id).saturating_add(1);
4190                if self.resolved_arrow_sheets.len() < len {
4191                    self.resolved_arrow_sheets.resize(len, None);
4192                }
4193                if let Some(slot) = self.resolved_arrow_sheets.get_mut(usize::from(id)) {
4194                    *slot = Some(index);
4195                }
4196            }
4197        }
4198        self.resolution_sheet_revision = Some(revision);
4199    }
4200
4201    /// Enable request-operation evidence before ingestion/evaluation.
4202    /// Each evaluation request resets events; this is not retained-result coverage.
4203    ///
4204    /// # Panics
4205    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
4206    pub fn enable_operation_diagnostics(&mut self, sample_limit: usize) {
4207        self.assert_not_poisoned();
4208        self.configure_operation_diagnostics(
4209            crate::operation_diagnostics::OperationDiagnosticOptions {
4210                sample_limit,
4211                physical_telemetry: true,
4212                ..Default::default()
4213            },
4214        );
4215    }
4216
4217    ///
4218    /// # Panics
4219    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
4220    pub fn configure_operation_diagnostics(
4221        &mut self,
4222        options: crate::operation_diagnostics::OperationDiagnosticOptions,
4223    ) {
4224        self.assert_not_poisoned();
4225        self.operation_diagnostic_collector = Some(Arc::new(
4226            crate::operation_diagnostics::OperationDiagnosticCollector::with_options(options),
4227        ));
4228    }
4229
4230    ///
4231    /// # Panics
4232    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
4233    pub fn disable_operation_diagnostics(&mut self) {
4234        self.assert_not_poisoned();
4235        self.operation_diagnostic_collector = None;
4236    }
4237
4238    pub fn operation_diagnostics(
4239        &self,
4240    ) -> Result<Option<crate::operation_diagnostics::OperationDiagnostics>, ExcelError> {
4241        self.operation_diagnostic_collector
4242            .as_ref()
4243            .map(|collector| collector.snapshot())
4244            .transpose()
4245    }
4246
4247    /// Finalize exact request evidence once, without disabling next-request collection.
4248    pub fn take_operation_diagnostics(
4249        &mut self,
4250    ) -> Result<Option<crate::operation_diagnostics::OperationDiagnostics>, ExcelError> {
4251        self.ensure_not_poisoned()?;
4252        self.operation_diagnostic_collector
4253            .as_mut()
4254            .map(|collector| collector.take_shared_snapshot())
4255            .transpose()
4256    }
4257
4258    pub(super) fn diagnostic_request_active(&self) -> bool {
4259        self.evaluation_resource_request_depth != 0
4260    }
4261
4262    fn begin_evaluation_request(&mut self) {
4263        self.refresh_resolution_snapshots();
4264        self.freshness_begin_request();
4265        #[cfg(test)]
4266        {
4267            self.evaluation_request_begin_count_for_test = self
4268                .evaluation_request_begin_count_for_test
4269                .saturating_add(1);
4270        }
4271        self.last_cycle_telemetry = CycleTelemetry::default();
4272        self.graph.authority_sync();
4273        // Defensive: consumed at the end of the previous request; a request
4274        // that errored out mid-walk must not leak its members into this one.
4275        self.pending_iterative_redirty.clear();
4276        self.reconcile_retained_sccs_at_request_begin();
4277        // Spec §7.11: NOW()/TODAY() sample the clock ONCE per recalc; every
4278        // read within this request (including SCC iteration passes) observes
4279        // this sample.
4280        self.clock.refresh();
4281    }
4282
4283    /// End-of-recalc redirty: volatile vertices (as always) plus members of
4284    /// SCCs that iterated this recalc without reaching a retainable fixed
4285    /// point (`CyclePolicy::Iterate`), so circular cells re-evaluate on every
4286    /// recalc exactly like Excel's iterative calculation (spec §4
4287    /// persistence / §7.6 accumulator / §7.11 volatile redirty). Retained
4288    /// SCCs (`retained_scc_members`, #368) are left clean. Replaces the bare
4289    /// `graph.redirty_volatiles()` call at every evaluation-flow exit; must
4290    /// run AFTER the flow's `clear_dirty_flags`.
4291    fn redirty_for_next_recalc(&mut self) {
4292        if self.snapshot_evaluation_active {
4293            self.snapshot_volatile_redirty_pending = true;
4294        } else {
4295            self.graph.redirty_volatiles();
4296        }
4297        let pending = std::mem::take(&mut self.pending_iterative_redirty);
4298        let dirty_at_begin = std::mem::take(&mut self.retained_scc_dirty_at_begin);
4299        for (vertex, scc) in dirty_at_begin {
4300            if self.retained_scc_members.get(&vertex) == Some(&scc) {
4301                // No SCC task claimed this member during the request: the
4302                // cycle dissolved and it evaluated as an ordinary formula,
4303                // or the request never reached it. Either way it is no
4304                // longer a retained fixed point. Its persisted value is
4305                // obsolete only if it actually re-evaluated (clean now).
4306                self.retained_scc_members.remove(&vertex);
4307                if !self.graph.is_dirty(vertex) {
4308                    self.iterative_state_values.remove(&vertex);
4309                }
4310            }
4311        }
4312        if !self.iterative_state_values.is_empty() || !self.retained_scc_members.is_empty() {
4313            let graph = &self.graph;
4314            self.iterative_state_values
4315                .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
4316            self.retained_scc_members
4317                .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
4318        }
4319        // Refresh the §4-persistence snapshot for members that re-run each
4320        // recalc: these final values survive structural edits that clear the
4321        // computed overlay (the only value home in canonical mode) so the
4322        // next SCC task can re-seed from them (see `iterative_state_values`).
4323        for &vertex in &pending {
4324            if !self.graph.is_live_formula_vertex(vertex) {
4325                continue;
4326            }
4327            if let Some(cell) = self.graph.get_cell_ref(vertex) {
4328                let sheet_name = self.graph.sheet_name(cell.sheet_id);
4329                match self.read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1) {
4330                    Some(value) if !matches!(value, LiteralValue::Empty) => {
4331                        self.iterative_state_values.insert(vertex, value);
4332                    }
4333                    _ => {
4334                        self.iterative_state_values.remove(&vertex);
4335                    }
4336                }
4337            }
4338        }
4339        if !pending.is_empty() {
4340            if self.snapshot_evaluation_active {
4341                self.snapshot_iterative_redirty_pending.extend(pending);
4342            } else {
4343                self.graph.redirty_iterative_members(&pending);
4344            }
4345        }
4346    }
4347
4348    /// Hash of every `EvalConfig` knob that can change the result of a
4349    /// retained SCC without any edit reaching the dependency graph. The
4350    /// function registry is tracked separately and precisely (see
4351    /// `retained_scc_function_epoch_seen`).
4352    fn retained_scc_config_fingerprint(&self) -> u64 {
4353        use std::hash::{Hash, Hasher};
4354        let mut hasher = rustc_hash::FxHasher::default();
4355        let config = &self.config;
4356        std::mem::discriminant(&config.cycle.detection).hash(&mut hasher);
4357        match config.cycle.policy {
4358            CyclePolicy::Error => 0u8.hash(&mut hasher),
4359            CyclePolicy::Iterate {
4360                max_iterations,
4361                max_change,
4362            } => {
4363                1u8.hash(&mut hasher);
4364                max_iterations.hash(&mut hasher);
4365                max_change.to_bits().hash(&mut hasher);
4366            }
4367        }
4368        config.date_system.hash(&mut hasher);
4369        config.workbook_seed.hash(&mut hasher);
4370        std::mem::discriminant(&config.volatile_level).hash(&mut hasher);
4371        // `DeterministicMode` carries a timestamp and a timezone spec; hash
4372        // its Debug rendering rather than growing its derive set for this.
4373        format!("{:?}", config.deterministic_mode).hash(&mut hasher);
4374        config.range_expansion_limit.hash(&mut hasher);
4375        config.max_open_ended_rows.hash(&mut hasher);
4376        config.max_open_ended_cols.hash(&mut hasher);
4377        hasher.finish()
4378    }
4379
4380    /// Request-begin bookkeeping for retained SCCs (#368): drop deleted
4381    /// vertices, invalidate everything when the config fingerprint moved
4382    /// (marking retained members dirty so their SCC tasks run in this
4383    /// request), and record how many retained SCCs are being reused, i.e.
4384    /// have no dirty member at request begin.
4385    fn reconcile_retained_sccs_at_request_begin(&mut self) {
4386        self.retained_scc_dirty_at_begin.clear();
4387        if self.retained_scc_members.is_empty() {
4388            return;
4389        }
4390        // Deleted vertices and members overwritten with a literal (the
4391        // vertex survives as a value cell) are no longer retained formulas.
4392        let graph = &self.graph;
4393        self.retained_scc_members
4394            .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
4395        if self.retained_scc_config_fingerprint() != self.retained_scc_config_fingerprint {
4396            let members: Vec<VertexId> = self.retained_scc_members.keys().copied().collect();
4397            self.retained_scc_members.clear();
4398            self.graph.mark_dirty_many(&members);
4399            return;
4400        }
4401        let changes =
4402            crate::function_registry::semantic_changes_since(self.retained_scc_function_epoch_seen);
4403        let global_changed = changes.epoch != self.retained_scc_function_epoch_seen;
4404        let provider_revision = self.resolver.planning_semantic_revision();
4405        let provider_changed = provider_revision != self.retained_scc_provider_revision_seen;
4406        if global_changed || provider_changed {
4407            let changed: BTreeSet<(String, String)> = changes.keys.into_iter().collect();
4408            let affected: Vec<VertexId> = self
4409                .retained_scc_members
4410                .keys()
4411                .copied()
4412                .filter(|&vertex| {
4413                    let Some(ast) = self.graph.get_formula(vertex) else {
4414                        return true;
4415                    };
4416                    (global_changed
4417                        && (!changes.complete || Self::ast_uses_changed_function(&ast, &changed)))
4418                        || (provider_changed && Self::ast_contains_function(&ast))
4419                })
4420                .collect();
4421            for vertex in &affected {
4422                self.retained_scc_members.remove(vertex);
4423            }
4424            if !affected.is_empty() {
4425                self.graph.mark_dirty_many(&affected);
4426            }
4427            self.retained_scc_function_epoch_seen = changes.epoch;
4428            self.retained_scc_provider_revision_seen = provider_revision;
4429        }
4430        let mut dirty_sccs: FxHashSet<u64> = FxHashSet::default();
4431        let mut all_sccs: FxHashSet<u64> = FxHashSet::default();
4432        for (&vertex, &scc) in &self.retained_scc_members {
4433            all_sccs.insert(scc);
4434            if self.graph.is_dirty(vertex) {
4435                dirty_sccs.insert(scc);
4436                self.retained_scc_dirty_at_begin.push((vertex, scc));
4437            }
4438        }
4439        let reused_members = self
4440            .retained_scc_members
4441            .values()
4442            .filter(|scc| !dirty_sccs.contains(scc))
4443            .count();
4444        let t = &mut self.last_cycle_telemetry;
4445        t.reused_sccs = all_sccs.len() - dirty_sccs.len();
4446        t.reused_scc_members = reused_members;
4447    }
4448
4449    pub fn virtual_dep_fallback_activations(&self) -> u64 {
4450        self.virtual_dep_fallback_activations
4451    }
4452
4453    #[cfg(test)]
4454    pub(crate) fn lookup_index_flights_built_for_test(&self) -> usize {
4455        self.lookup_index_cache
4456            .flights_built
4457            .load(std::sync::atomic::Ordering::Relaxed)
4458    }
4459
4460    pub(crate) fn last_lookup_index_cache_report(&self) -> LookupIndexCacheReport {
4461        self.lookup_index_cache.report()
4462    }
4463
4464    fn lookup_view_contains_volatile(&self, view: &RangeView<'_>, sheet_id: SheetId) -> bool {
4465        let start_row = view.start_row();
4466        let end_row = view.end_row();
4467        let start_col = view.start_col();
4468        let end_col = view.end_col();
4469        for row in start_row..=end_row {
4470            let Ok(row_u32) = u32::try_from(row) else {
4471                return true;
4472            };
4473            for col in start_col..=end_col {
4474                let Ok(col_u32) = u32::try_from(col) else {
4475                    return true;
4476                };
4477                let cell_ref = self
4478                    .graph
4479                    .make_cell_ref_internal(sheet_id, row_u32, col_u32);
4480                if let Some(vertex_id) = self.graph.get_vertex_id_for_address(&cell_ref)
4481                    && self.graph.is_volatile(vertex_id)
4482                {
4483                    return true;
4484                }
4485            }
4486        }
4487        false
4488    }
4489
4490    fn build_lookup_index_impl(
4491        &self,
4492        view: &RangeView<'_>,
4493        axis: LookupAxis,
4494    ) -> Option<Arc<LookupIndex>> {
4495        let (rows, cols) = view.dims();
4496        if rows == 0 || cols == 0 {
4497            self.lookup_index_cache.note_skipped_tiny();
4498            return None;
4499        }
4500        let len = match axis {
4501            LookupAxis::ColumnInView(col) => {
4502                if col >= cols {
4503                    self.lookup_index_cache.note_skipped_tiny();
4504                    return None;
4505                }
4506                rows
4507            }
4508            LookupAxis::RowInView(row) => {
4509                if row >= rows {
4510                    self.lookup_index_cache.note_skipped_tiny();
4511                    return None;
4512                }
4513                cols
4514            }
4515        };
4516        if len < 64 {
4517            self.lookup_index_cache.note_skipped_tiny();
4518            return None;
4519        }
4520
4521        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
4522        let key = LookupIndexKey {
4523            sheet_id,
4524            start_row: u32::try_from(view.start_row()).ok()?,
4525            start_col: u32::try_from(view.start_col()).ok()?,
4526            end_row: u32::try_from(view.end_row()).ok()?,
4527            end_col: u32::try_from(view.end_col()).ok()?,
4528            axis,
4529            snapshot_id: self.data_snapshot_id(),
4530        };
4531        if let Some(index) = self.lookup_index_cache.get(&key) {
4532            return Some(index);
4533        }
4534        if self
4535            .lookup_index_cache
4536            .would_exceed_cap(estimate_bytes(len, 0))
4537        {
4538            self.lookup_index_cache.note_skipped_cap();
4539            return None;
4540        }
4541        if !self.lookup_index_cache.should_build(key) {
4542            return None;
4543        }
4544        // Parallel members of a lookup family miss together: one builds.
4545        self.lookup_index_cache.single_flight(key, || {
4546            if let Some(index) = self.lookup_index_cache.recheck(&key) {
4547                return Some(index);
4548            }
4549            if self.lookup_index_cache.is_known_volatile(&key) {
4550                self.lookup_index_cache.note_skipped_volatile();
4551                return None;
4552            }
4553            if self.lookup_view_contains_volatile(view, sheet_id) {
4554                self.lookup_index_cache.note_volatile_key(key);
4555                self.lookup_index_cache.note_skipped_volatile();
4556                return None;
4557            }
4558            #[cfg(test)]
4559            self.lookup_index_cache
4560                .flights_built
4561                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
4562            match LookupIndex::build(view, axis, self.config.date_system).ok()? {
4563                BuildOutcome::Built(index) => self.lookup_index_cache.insert_if_room(key, index),
4564                BuildOutcome::ErrorInLookupAxis => {
4565                    self.lookup_index_cache.note_skipped_error();
4566                    None
4567                }
4568                BuildOutcome::Degenerate => {
4569                    self.lookup_index_cache.note_skipped_tiny();
4570                    None
4571                }
4572            }
4573        })
4574    }
4575
4576    fn reset_virtual_dep_telemetry_if_disabled(&mut self) {
4577        if !self.config.enable_virtual_dep_telemetry {
4578            self.last_virtual_dep_telemetry = VirtualDepTelemetry {
4579                fallback_mode_activations: self.virtual_dep_fallback_activations,
4580                ..VirtualDepTelemetry::default()
4581            };
4582        }
4583    }
4584
4585    fn source_cache_session(&self) -> SourceCacheSession {
4586        self.clear_source_cache();
4587        SourceCacheSession {
4588            cache: self.source_cache.clone(),
4589        }
4590    }
4591
4592    fn resolve_source_scalar_cached(
4593        &self,
4594        name: &str,
4595        version: Option<u64>,
4596    ) -> Result<LiteralValue, ExcelError> {
4597        let key = (name.to_string(), version);
4598        if let Ok(mut g) = self.source_cache.write() {
4599            if let Some(v) = g.scalars.get(&key) {
4600                return Ok(v.clone());
4601            }
4602
4603            let v = self.resolver.resolve_source_scalar(name).map_err(|err| {
4604                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4605                    ExcelError::new(ExcelErrorKind::Ref)
4606                        .with_message(format!("Unresolved source scalar: {name}"))
4607                } else {
4608                    err
4609                }
4610            })?;
4611            g.scalars.insert(key, v.clone());
4612            Ok(v)
4613        } else {
4614            self.resolver.resolve_source_scalar(name).map_err(|err| {
4615                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4616                    ExcelError::new(ExcelErrorKind::Ref)
4617                        .with_message(format!("Unresolved source scalar: {name}"))
4618                } else {
4619                    err
4620                }
4621            })
4622        }
4623    }
4624
4625    fn resolve_source_table_cached(
4626        &self,
4627        name: &str,
4628        version: Option<u64>,
4629    ) -> Result<Arc<dyn crate::traits::Table>, ExcelError> {
4630        let key = (name.to_string(), version);
4631        if let Ok(mut g) = self.source_cache.write() {
4632            if let Some(t) = g.tables.get(&key) {
4633                return Ok(t.clone());
4634            }
4635
4636            let t = self.resolver.resolve_source_table(name).map_err(|err| {
4637                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4638                    ExcelError::new(ExcelErrorKind::Ref)
4639                        .with_message(format!("Unresolved source table: {name}"))
4640                } else {
4641                    err
4642                }
4643            })?;
4644            let t: Arc<dyn crate::traits::Table> = Arc::from(t);
4645            g.tables.insert(key, t.clone());
4646            Ok(t)
4647        } else {
4648            self.resolver
4649                .resolve_source_table(name)
4650                .map_err(|err| {
4651                    if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4652                        ExcelError::new(ExcelErrorKind::Ref)
4653                            .with_message(format!("Unresolved source table: {name}"))
4654                    } else {
4655                        err
4656                    }
4657                })
4658                .map(Arc::from)
4659        }
4660    }
4661
4662    fn source_table_to_range_view(
4663        &self,
4664        table: &dyn crate::traits::Table,
4665        spec: &Option<formualizer_parse::parser::TableSpecifier>,
4666    ) -> Result<RangeView<'static>, ExcelError> {
4667        let result = self.source_table_to_range_view_inner(table, spec);
4668        if let Some(error) = crate::external_callbacks::pending() {
4669            Err(error)
4670        } else {
4671            result
4672        }
4673    }
4674
4675    fn source_table_to_range_view_inner(
4676        &self,
4677        table: &dyn crate::traits::Table,
4678        spec: &Option<formualizer_parse::parser::TableSpecifier>,
4679    ) -> Result<RangeView<'static>, ExcelError> {
4680        use formualizer_parse::parser::{SpecialItem, TableSpecifier};
4681
4682        let owned = match spec {
4683            Some(TableSpecifier::Column(c)) => {
4684                let c = c.trim();
4685                if c == "@" || c.contains('[') || c.contains(']') || c.contains(',') {
4686                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
4687                        "Complex structured references not yet supported".to_string(),
4688                    ));
4689                }
4690                table.get_column(c)?.materialise().into_owned()
4691            }
4692            Some(TableSpecifier::ColumnRange(start, end)) => {
4693                let cols = table.columns();
4694                let start = start.trim();
4695                let end = end.trim();
4696                let start_key = start.to_lowercase();
4697                let end_key = end.to_lowercase();
4698                let start_idx = cols.iter().position(|n| n.to_lowercase() == start_key);
4699                let end_idx = cols.iter().position(|n| n.to_lowercase() == end_key);
4700                if let (Some(mut si), Some(mut ei)) = (start_idx, end_idx) {
4701                    if si > ei {
4702                        std::mem::swap(&mut si, &mut ei);
4703                    }
4704                    let h = table.data_height();
4705                    let w = ei - si + 1;
4706                    let mut rows = vec![vec![LiteralValue::Empty; w]; h];
4707                    for (offset, ci) in (si..=ei).enumerate() {
4708                        let cname = &cols[ci];
4709                        let col_range = table.get_column(cname)?;
4710                        let (rh, _) = col_range.dimensions();
4711                        for (r, row) in rows.iter_mut().enumerate().take(h.min(rh)) {
4712                            row[offset] = col_range.get(r, 0)?;
4713                        }
4714                    }
4715                    rows
4716                } else {
4717                    return Err(ExcelError::new(ExcelErrorKind::Ref)
4718                        .with_message("Column range refers to unknown column(s)".to_string()));
4719                }
4720            }
4721            Some(TableSpecifier::SpecialItem(SpecialItem::Headers))
4722            | Some(TableSpecifier::Headers) => table
4723                .headers_row()
4724                .map(|r| r.materialise().into_owned())
4725                .unwrap_or_default(),
4726            Some(TableSpecifier::SpecialItem(SpecialItem::Totals))
4727            | Some(TableSpecifier::Totals) => table
4728                .totals_row()
4729                .map(|r| r.materialise().into_owned())
4730                .unwrap_or_default(),
4731            Some(TableSpecifier::SpecialItem(SpecialItem::Data)) | Some(TableSpecifier::Data) => {
4732                table
4733                    .data_body()
4734                    .map(|r| r.materialise().into_owned())
4735                    .unwrap_or_default()
4736            }
4737            Some(TableSpecifier::SpecialItem(SpecialItem::All)) | Some(TableSpecifier::All) => {
4738                let mut out: Vec<Vec<LiteralValue>> = Vec::new();
4739                if let Some(h) = table.headers_row() {
4740                    out.extend(h.iter_rows());
4741                }
4742                if let Some(body) = table.data_body() {
4743                    out.extend(body.iter_rows());
4744                }
4745                if let Some(tr) = table.totals_row() {
4746                    out.extend(tr.iter_rows());
4747                }
4748                out
4749            }
4750            Some(TableSpecifier::SpecialItem(SpecialItem::ThisRow)) => {
4751                return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
4752                    "@ (This Row) requires table-aware context; not yet supported".to_string(),
4753                ));
4754            }
4755            Some(TableSpecifier::Row(_)) | Some(TableSpecifier::Combination(_)) => {
4756                return Err(ExcelError::new(ExcelErrorKind::NImpl)
4757                    .with_message("Complex structured references not yet supported".to_string()));
4758            }
4759            None => {
4760                return Err(ExcelError::new(ExcelErrorKind::NImpl)
4761                    .with_message("Table reference without specifier is unsupported".to_string()));
4762            }
4763        };
4764
4765        Ok(RangeView::from_owned_rows(owned, self.config.date_system))
4766    }
4767
4768    pub fn default_sheet_id(&self) -> SheetId {
4769        self.graph.default_sheet_id()
4770    }
4771
4772    pub fn default_sheet_name(&self) -> &str {
4773        self.graph.default_sheet_name()
4774    }
4775
4776    /// Update the workbook seed for deterministic RNGs in functions.
4777    ///
4778    /// # Panics
4779    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
4780    pub fn set_workbook_seed(&mut self, seed: u64) {
4781        self.assert_not_poisoned();
4782        self.config.workbook_seed = seed;
4783    }
4784
4785    /// Set the volatile level policy (Always/OnRecalc/OnOpen)
4786    ///
4787    /// # Panics
4788    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
4789    pub fn set_volatile_level(&mut self, level: crate::traits::VolatileLevel) {
4790        self.assert_not_poisoned();
4791        self.config.volatile_level = level;
4792    }
4793
4794    /// Set public temporal materialisation to native values or raw serials.
4795    ///
4796    /// # Panics
4797    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
4798    pub fn set_temporal_egress(&mut self, policy: crate::engine::TemporalEgress) {
4799        self.assert_not_poisoned();
4800        self.config.temporal_egress = policy;
4801    }
4802
4803    pub fn temporal_egress(&self) -> crate::engine::TemporalEgress {
4804        self.config.temporal_egress
4805    }
4806
4807    /// Enable/disable deterministic evaluation mode (fixed clock + timezone).
4808    pub fn set_deterministic_mode(
4809        &mut self,
4810        mode: crate::engine::DeterministicMode,
4811    ) -> Result<(), ExcelError> {
4812        self.ensure_not_poisoned()?;
4813        let clock = mode.build_clock()?;
4814        self.config.deterministic_mode = mode;
4815        self.clock = crate::timezone::SnapshotClock::new(clock);
4816        Ok(())
4817    }
4818
4819    /// Inject a custom [`ClockProvider`](crate::timezone::ClockProvider) for
4820    /// volatile date/time builtins (`NOW()`, `TODAY()`).
4821    ///
4822    /// The provider is the clock *source*; per spec §7.11 the engine samples
4823    /// it once at the start of every evaluation request and all reads within
4824    /// that recalc (including SCC iteration passes) observe the frozen
4825    /// sample.
4826    ///
4827    /// # Panics
4828    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
4829    pub fn set_clock(&mut self, clock: Arc<dyn crate::timezone::ClockProvider>) {
4830        self.assert_not_poisoned();
4831        self.clock = crate::timezone::SnapshotClock::new(clock);
4832    }
4833
4834    fn validate_deterministic_mode(&self) -> Result<(), ExcelError> {
4835        self.config.deterministic_mode.validate()
4836    }
4837
4838    pub fn sheet_id(&self, name: &str) -> Option<SheetId> {
4839        self.graph.sheet_id(name)
4840    }
4841
4842    ///
4843    /// # Panics
4844    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
4845    pub fn sheet_id_mut(&mut self, name: &str) -> SheetId {
4846        self.assert_not_poisoned();
4847        self.add_sheet(name)
4848            .unwrap_or_else(|_| self.graph.sheet_id_mut(name))
4849    }
4850
4851    pub fn sheet_name(&self, id: SheetId) -> &str {
4852        self.graph.sheet_name(id)
4853    }
4854
4855    fn arrow_sheet_position(&self, id: SheetId) -> Option<usize> {
4856        let index = self.arrow_sheet_index.get_or_init(|| {
4857            let mut index = Vec::new();
4858            for (position, sheet) in self.arrow_sheets.sheets.iter().enumerate() {
4859                if let Some(id) = self.graph.sheet_id(&sheet.name) {
4860                    let needed = usize::from(id).saturating_add(1);
4861                    if index.len() < needed {
4862                        index.resize(needed, None);
4863                    }
4864                    if let Some(slot) = index.get_mut(usize::from(id)) {
4865                        *slot = Some(position);
4866                    }
4867                }
4868            }
4869            index
4870        });
4871        index.get(usize::from(id)).copied().flatten()
4872    }
4873
4874    /// Indexed Arrow storage lookup by stable registry identity.
4875    pub(crate) fn arrow_sheet_by_id(&self, id: SheetId) -> Option<&crate::arrow_store::ArrowSheet> {
4876        self.arrow_sheets.sheets.get(self.arrow_sheet_position(id)?)
4877    }
4878
4879    /// Mutable counterpart of [`Self::arrow_sheet_by_id`].
4880    pub(crate) fn arrow_sheet_by_id_mut(
4881        &mut self,
4882        id: SheetId,
4883    ) -> Option<&mut crate::arrow_store::ArrowSheet> {
4884        let position = self.arrow_sheet_position(id)?;
4885        self.arrow_sheets.sheets.get_mut(position)
4886    }
4887
4888    fn rebuild_arrow_sheet_index(&mut self) {
4889        self.arrow_sheet_index.take();
4890        self.resolved_arrow_sheets.clear();
4891        for (index, sheet) in self.arrow_sheets.sheets.iter().enumerate() {
4892            if let Some(id) = self.graph.sheet_id(&sheet.name) {
4893                let len = usize::from(id).saturating_add(1);
4894                if self.resolved_arrow_sheets.len() < len {
4895                    self.resolved_arrow_sheets.resize(len, None);
4896                }
4897                if let Some(slot) = self.resolved_arrow_sheets.get_mut(usize::from(id)) {
4898                    *slot = Some(index);
4899                }
4900            }
4901        }
4902    }
4903
4904    pub fn add_sheet(&mut self, name: &str) -> Result<SheetId, ExcelError> {
4905        self.ensure_not_poisoned()?;
4906        let id = self.graph.add_sheet(name)?;
4907        self.ensure_arrow_sheet(name);
4908        self.mark_topology_edited();
4909        Ok(id)
4910    }
4911
4912    pub fn duplicate_sheet(&mut self, source: &str, new_name: &str) -> Result<SheetId, ExcelError> {
4913        self.ensure_not_poisoned()?;
4914        let source_id = self.graph.sheet_id(source).ok_or_else(|| {
4915            ExcelError::new(ExcelErrorKind::Value).with_message("Source sheet does not exist")
4916        })?;
4917        if new_name.is_empty() || new_name.len() > 255 {
4918            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
4919        }
4920        if self.graph.sheet_id(new_name).is_some() {
4921            return Err(ExcelError::new(ExcelErrorKind::Value)
4922                .with_message(format!("Sheet '{new_name}' already exists")));
4923        }
4924        let new_id = self.graph.duplicate_sheet(source_id, new_name)?;
4925
4926        if let Some(source_sheet) = self.arrow_sheets.sheet(source).cloned() {
4927            let mut copied_sheet = source_sheet;
4928            copied_sheet.name = Arc::<str>::from(new_name);
4929            self.arrow_sheets.sheets.push(copied_sheet);
4930            self.rebuild_arrow_sheet_index();
4931        } else {
4932            self.ensure_arrow_sheet(new_name);
4933        }
4934
4935        let duplicated_formulas = self
4936            .graph
4937            .formula_vertices()
4938            .into_iter()
4939            .filter(|vertex| {
4940                self.graph
4941                    .get_cell_ref(*vertex)
4942                    .is_some_and(|cell| cell.sheet_id == new_id)
4943            })
4944            .collect::<Vec<_>>();
4945        self.graph.mark_vertices_dirty_batch(&duplicated_formulas);
4946        self.mark_topology_edited();
4947        Ok(new_id)
4948    }
4949
4950    fn ensure_arrow_sheet(&mut self, name: &str) {
4951        if self.arrow_sheets.sheet(name).is_some() {
4952            return;
4953        }
4954        self.arrow_sheets
4955            .sheets
4956            .push(crate::arrow_store::ArrowSheet {
4957                name: std::sync::Arc::<str>::from(name),
4958                date_system: self.config.date_system,
4959                columns: Vec::new(),
4960                nrows: 0,
4961                chunk_starts: Vec::new(),
4962                chunk_rows: 32 * 1024,
4963            });
4964        self.rebuild_arrow_sheet_index();
4965    }
4966
4967    pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
4968        self.ensure_not_poisoned()?;
4969        let name = self.graph.sheet_name(sheet_id).to_string();
4970        self.purge_derived_formats_for_sheet(sheet_id);
4971        self.graph.remove_sheet(sheet_id)?;
4972        self.arrow_sheets.sheets.retain(|s| s.name.as_ref() != name);
4973        self.rebuild_arrow_sheet_index();
4974        // Sheet removal can change cross-sheet refs, names, and default-sheet
4975        // resolution. Until those domains have a complete exact dependency
4976        // proof, retain the documented graph-owned global invalidation.
4977        self.clear_all_computed_overlays();
4978        self.mark_all_formula_vertices_dirty();
4979        self.clear_staged_formulas_for_sheet(&name);
4980        if self.row_visibility.remove(&sheet_id).is_some() {
4981            self.invalidate_row_visibility_mask_cache();
4982        }
4983        self.record_structural_change(StructuralScope::RemovedSheet(sheet_id));
4984        self.mark_topology_edited();
4985        Ok(())
4986    }
4987
4988    /// Helper to synchronize the Arrow-backed storage layer.
4989    fn rename_sheet_in_arrow_store(&mut self, target_name: &str, new_name: &str) -> bool {
4990        if let Some(asheet) = self
4991            .arrow_sheets
4992            .sheets
4993            .iter_mut()
4994            .find(|s| s.name.as_ref() == target_name)
4995        {
4996            asheet.name = std::sync::Arc::<str>::from(new_name);
4997            return true;
4998        }
4999        false
5000    }
5001
5002    pub fn rename_sheet(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
5003        self.ensure_not_poisoned()?;
5004        let old_name = self.graph.sheet_name(sheet_id).to_string();
5005
5006        // Speculative Storage Update
5007        // Update name in storage FIRST so the Evaluator can find it during Graph rescue.
5008        self.rename_sheet_in_arrow_store(&old_name, new_name);
5009
5010        // Graph Update (Metadata + Rescue Logic)
5011        match self.graph.rename_sheet(sheet_id, new_name) {
5012            Ok(_) => {
5013                self.rename_staged_formula_sheet(&old_name, new_name);
5014                // Success! Invalidate cache for the moved sheet
5015                let sheet_vertices: Vec<VertexId> = self
5016                    .graph
5017                    .grid_vertices_in_sheet(sheet_id)
5018                    .map(|(id, _)| id)
5019                    .collect();
5020                for v_id in sheet_vertices {
5021                    self.graph.mark_vertex_dirty(v_id);
5022                }
5023                // Sheet rename preserves SheetId and therefore formula dependencies.
5024                self.mark_topology_edited();
5025                Ok(())
5026            }
5027            Err(e) => {
5028                // ROLLBACK: Revert storage if graph rejected the name
5029                self.rename_sheet_in_arrow_store(new_name, &old_name);
5030                Err(e)
5031            }
5032        }
5033    }
5034
5035    pub fn named_ranges_iter(
5036        &self,
5037    ) -> impl Iterator<Item = (&String, &crate::engine::named_range::NamedRange)> {
5038        self.graph.named_ranges_iter()
5039    }
5040
5041    pub fn sheet_named_ranges_iter(
5042        &self,
5043    ) -> impl Iterator<Item = (&(SheetId, String), &crate::engine::named_range::NamedRange)> {
5044        self.graph.sheet_named_ranges_iter()
5045    }
5046
5047    pub fn resolve_name_entry(
5048        &self,
5049        name: &str,
5050        current_sheet: SheetId,
5051    ) -> Option<&crate::engine::named_range::NamedRange> {
5052        self.graph.resolve_name_entry(name, current_sheet)
5053    }
5054
5055    /// The [`NameScope`] an optional scope-sheet argument denotes.
5056    ///
5057    /// `None` means **workbook scope**, not "the default sheet": a caller that
5058    /// supplies no sheet context is asking about workbook-scoped names only.
5059    /// An unknown sheet name is a malformed query and errors rather than
5060    /// silently degrading to another sheet's scope (issue #110).
5061    ///
5062    /// This is the one owned derivation from `Option<&str>` to a name scope;
5063    /// every scope-taking entry point routes through it.
5064    pub(crate) fn name_query_scope(
5065        &self,
5066        scope_sheet: Option<&str>,
5067    ) -> Result<NameScope, ExcelError> {
5068        match scope_sheet {
5069            None => Ok(NameScope::Workbook),
5070            Some(sheet) => self
5071                .graph
5072                .sheet_id(sheet)
5073                .map(NameScope::Sheet)
5074                .ok_or_else(|| {
5075                    ExcelError::new(ExcelErrorKind::Ref)
5076                        .with_message(format!("name scope sheet not found: {sheet}"))
5077                }),
5078        }
5079    }
5080
5081    /// Whether `name` resolves in the scope denoted by `scope_sheet`.
5082    ///
5083    /// `scope_sheet == None` asks about workbook scope only; a name scoped to a
5084    /// single sheet (including the default sheet) does not answer it. An unknown
5085    /// sheet name resolves nothing.
5086    /// Resolve a [`SharedSheetLocator`](crate::reference::SharedSheetLocator)
5087    /// against an explicit context sheet.
5088    ///
5089    /// Thin forwarder to
5090    /// [`SheetRegistry::resolve_locator`](crate::engine::sheet_registry::SheetRegistry::resolve_locator),
5091    /// the single owned derivation. `Current` resolves to `context_sheet`, never
5092    /// to the workbook's default sheet.
5093    fn resolve_sheet_locator(
5094        &self,
5095        locator: &crate::reference::SharedSheetLocator<'_>,
5096        context_sheet: SheetId,
5097    ) -> Result<SheetId, ExcelError> {
5098        self.graph
5099            .sheet_reg()
5100            .resolve_locator(locator, context_sheet)
5101    }
5102
5103    pub fn has_name(&self, name: &str, scope_sheet: Option<&str>) -> bool {
5104        let Ok(scope) = self.name_query_scope(scope_sheet) else {
5105            return false;
5106        };
5107        self.graph
5108            .resolve_name_entry_in_scope(name, scope)
5109            .is_some()
5110    }
5111
5112    /// The current value of `name` in the scope denoted by `scope_sheet`.
5113    ///
5114    /// Scoping follows [`Self::has_name`]: `None` is workbook scope only.
5115    pub fn resolved_name_value(
5116        &self,
5117        name: &str,
5118        scope_sheet: Option<&str>,
5119    ) -> Option<LiteralValue> {
5120        let scope = self.name_query_scope(scope_sheet).ok()?;
5121        let entry = self.graph.resolve_name_entry_in_scope(name, scope)?;
5122        self.graph.get_value(entry.vertex)
5123    }
5124
5125    /// Whether a cell lies in a logically empty table's reserved body.
5126    pub fn cell_in_empty_table(&self, sheet: &str, row: u32, col: u32) -> bool {
5127        let Some(sheet_id) = self.graph.sheet_id(sheet) else {
5128            return false;
5129        };
5130        let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
5131        !self.graph.empty_tables_at(cell).is_empty()
5132    }
5133
5134    pub fn table_metadata(&self, name: &str) -> Option<TableMetadata> {
5135        let entry = self.graph.resolve_table_entry(name)?;
5136        Some(TableMetadata {
5137            name: entry.name.clone(),
5138            sheet: self.graph.sheet_name(entry.sheet_id()).to_string(),
5139            start_row: entry.range.start.coord.row() + 1,
5140            start_col: entry.range.start.coord.col() + 1,
5141            end_row: entry.range.end.coord.row() + 1,
5142            end_col: entry.range.end.coord.col() + 1,
5143            header_row: entry.header_row,
5144            headers: entry.headers.clone(),
5145            totals_row: entry.totals_row,
5146            empty_body: entry.empty_body,
5147        })
5148    }
5149
5150    /// Metadata for every defined table, ordered by name.
5151    pub fn tables(&self) -> Vec<TableMetadata> {
5152        self.graph
5153            .table_names()
5154            .into_iter()
5155            .filter_map(|name| self.table_metadata(&name))
5156            .collect()
5157    }
5158
5159    pub fn named_ranges_snapshot(&self) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
5160        let mut out: Vec<crate::engine::named_range::NamedRangeSnapshot> = Vec::new();
5161
5162        for (name, named) in self.graph.named_ranges_iter() {
5163            out.push(crate::engine::named_range::NamedRangeSnapshot {
5164                name: name.clone(),
5165                scope: NameScope::Workbook,
5166                definition: named.definition.clone(),
5167            });
5168        }
5169
5170        for ((sheet_id, name), named) in self.graph.sheet_named_ranges_iter() {
5171            out.push(crate::engine::named_range::NamedRangeSnapshot {
5172                name: name.clone(),
5173                scope: NameScope::Sheet(*sheet_id),
5174                definition: named.definition.clone(),
5175            });
5176        }
5177
5178        out.sort_by(|a, b| {
5179            let a_scope = match a.scope {
5180                NameScope::Workbook => (0u8, 0u32),
5181                NameScope::Sheet(id) => (1u8, u32::from(id)),
5182            };
5183            let b_scope = match b.scope {
5184                NameScope::Workbook => (0u8, 0u32),
5185                NameScope::Sheet(id) => (1u8, u32::from(id)),
5186            };
5187            a_scope.cmp(&b_scope).then_with(|| a.name.cmp(&b.name))
5188        });
5189
5190        out
5191    }
5192
5193    pub fn named_ranges_snapshot_for_sheet(
5194        &self,
5195        sheet_id: SheetId,
5196    ) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
5197        self.named_ranges_snapshot()
5198            .into_iter()
5199            .filter(|entry| match entry.scope {
5200                NameScope::Workbook => true,
5201                NameScope::Sheet(id) => id == sheet_id,
5202            })
5203            .collect()
5204    }
5205
5206    pub fn define_name(
5207        &mut self,
5208        name: &str,
5209        definition: NamedDefinition,
5210        scope: NameScope,
5211    ) -> Result<(), ExcelError> {
5212        self.ensure_not_poisoned()?;
5213        self.graph.validate_define_name(name, scope)?;
5214        self.graph.define_name(name, definition, scope)?;
5215        self.record_structural_change(StructuralScope::AllSheets);
5216
5217        self.mark_topology_edited();
5218
5219        Ok(())
5220    }
5221
5222    pub fn update_name(
5223        &mut self,
5224        name: &str,
5225        definition: NamedDefinition,
5226        scope: NameScope,
5227    ) -> Result<(), ExcelError> {
5228        self.ensure_not_poisoned()?;
5229        self.graph.validate_existing_name(name, scope)?;
5230        self.graph.update_name(name, definition, scope)?;
5231        self.record_structural_change(StructuralScope::AllSheets);
5232
5233        self.mark_topology_edited();
5234
5235        Ok(())
5236    }
5237
5238    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
5239        self.ensure_not_poisoned()?;
5240        self.graph.validate_existing_name(name, scope)?;
5241        self.graph.delete_name(name, scope)?;
5242        self.record_structural_change(StructuralScope::AllSheets);
5243
5244        self.mark_topology_edited();
5245
5246        Ok(())
5247    }
5248
5249    pub fn define_table(
5250        &mut self,
5251        name: &str,
5252        range: crate::reference::RangeRef,
5253        header_row: bool,
5254        headers: Vec<String>,
5255        totals_row: bool,
5256    ) -> Result<(), ExcelError> {
5257        self.ensure_not_poisoned()?;
5258        self.graph
5259            .define_table(name, range, header_row, headers, totals_row)?;
5260        self.record_structural_change(StructuralScope::AllSheets);
5261        self.mark_topology_edited();
5262        Ok(())
5263    }
5264
5265    fn table_resize_range(&self, name: &str, new_end_row: u32) -> Result<RangeRef, ExcelError> {
5266        let entry = self.graph.resolve_table_entry(name).ok_or_else(|| {
5267            ExcelError::new(ExcelErrorKind::Name).with_message(format!("Unknown table: {name}"))
5268        })?;
5269        let minimum = entry.range.start.coord.row()
5270            + 1
5271            + u32::from(entry.header_row)
5272            + u32::from(entry.totals_row);
5273        if new_end_row < minimum || new_end_row > 1_048_576 {
5274            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
5275                "Table resize must retain at least one body row within the sheet grid",
5276            ));
5277        }
5278        let mut range = entry.range;
5279        range.end.coord = Coord::from_excel(new_end_row, range.end.coord.col() + 1, true, true);
5280        Ok(range)
5281    }
5282
5283    /// Resize a native table's row extent. For history, use an EngineAction.
5284    pub fn resize_table(
5285        &mut self,
5286        name: &str,
5287        new_end_row: u32,
5288    ) -> Result<TableMetadata, ExcelError> {
5289        self.resize_table_body(name, new_end_row, false)
5290    }
5291
5292    /// Resize a table, retaining a physical row for a logically empty body.
5293    pub fn resize_table_body(
5294        &mut self,
5295        name: &str,
5296        new_end_row: u32,
5297        empty_body: bool,
5298    ) -> Result<TableMetadata, ExcelError> {
5299        self.ensure_not_poisoned()?;
5300        let range = self.table_resize_range(name, new_end_row)?;
5301        self.graph.resize_table_body(name, range, empty_body)?;
5302        self.record_structural_change(StructuralScope::AllSheets);
5303        self.mark_topology_edited();
5304        Ok(self.table_metadata(name).expect("resized table exists"))
5305    }
5306
5307    pub fn define_source_scalar(
5308        &mut self,
5309        name: &str,
5310        version: Option<u64>,
5311    ) -> Result<(), ExcelError> {
5312        self.ensure_not_poisoned()?;
5313        self.graph.define_source_scalar(name, version)?;
5314        self.record_structural_change(StructuralScope::OpaqueGlobal);
5315        self.mark_topology_edited();
5316        Ok(())
5317    }
5318
5319    pub fn define_source_table(
5320        &mut self,
5321        name: &str,
5322        version: Option<u64>,
5323    ) -> Result<(), ExcelError> {
5324        self.ensure_not_poisoned()?;
5325        self.graph.define_source_table(name, version)?;
5326        self.record_structural_change(StructuralScope::OpaqueGlobal);
5327        self.mark_topology_edited();
5328        Ok(())
5329    }
5330
5331    pub fn set_source_scalar_version(
5332        &mut self,
5333        name: &str,
5334        version: Option<u64>,
5335    ) -> Result<(), ExcelError> {
5336        self.ensure_not_poisoned()?;
5337        self.graph.set_source_scalar_version(name, version)?;
5338        self.source_mutation_generation = self.source_mutation_generation.wrapping_add(1);
5339        Ok(())
5340    }
5341
5342    pub fn set_source_table_version(
5343        &mut self,
5344        name: &str,
5345        version: Option<u64>,
5346    ) -> Result<(), ExcelError> {
5347        self.ensure_not_poisoned()?;
5348        self.graph.set_source_table_version(name, version)?;
5349        self.source_mutation_generation = self.source_mutation_generation.wrapping_add(1);
5350        Ok(())
5351    }
5352
5353    pub fn invalidate_source(&mut self, name: &str) -> Result<(), ExcelError> {
5354        self.ensure_not_poisoned()?;
5355        self.graph.invalidate_source(name)?;
5356        self.source_mutation_generation = self.source_mutation_generation.wrapping_add(1);
5357        Ok(())
5358    }
5359
5360    pub fn vertex_value(&self, vertex: VertexId) -> Option<LiteralValue> {
5361        self.graph.get_value(vertex)
5362    }
5363
5364    pub fn graph_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
5365        self.graph.get_cell_value(sheet, row, col)
5366    }
5367
5368    /// Whether a cell belongs to a currently committed spill projection.
5369    ///
5370    /// This indexed ownership query does not read values, calculate spill
5371    /// bounds, prepare formulas, or evaluate the workbook.
5372    pub fn cell_has_spill_projection(&self, sheet: &str, row: u32, col: u32) -> bool {
5373        if self.graph.spill_registry_is_empty() {
5374            return false;
5375        }
5376        let Some(sheet_id) = self.graph.sheet_id(sheet) else {
5377            return false;
5378        };
5379        let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
5380        self.graph.spill_registry_anchor_for_cell(cell).is_some()
5381            || self
5382                .graph
5383                .get_vertex_for_cell(&cell)
5384                .is_some_and(|vertex| self.graph.spill_registry_has_anchor(vertex))
5385    }
5386
5387    pub fn vertex_for_cell(&self, cell: &CellRef) -> Option<VertexId> {
5388        self.graph.get_vertex_for_cell(cell)
5389    }
5390
5391    pub fn evaluation_vertices(&self) -> Vec<VertexId> {
5392        self.graph.get_evaluation_vertices()
5393    }
5394
5395    /// Return read-only baseline counters for dispatch benchmarking.
5396    pub fn baseline_stats(&self) -> EngineBaselineStats {
5397        let graph = self.graph.baseline_stats();
5398        EngineBaselineStats {
5399            graph_vertex_count: graph.graph_vertex_count,
5400            graph_formula_vertex_count: graph.graph_formula_vertex_count,
5401            graph_edge_count: graph.graph_edge_count,
5402            dirty_vertex_count: graph.dirty_vertex_count,
5403            evaluation_vertex_count: graph.evaluation_vertex_count,
5404            formula_ast_root_count: graph.formula_ast_root_count,
5405            formula_ast_node_count: graph.formula_ast_node_count,
5406            staged_formula_count: self.staged_formula_count(),
5407            formula_plane_active_span_count: 0,
5408            formula_plane_producer_result_entries: 0,
5409            formula_plane_consumer_read_entries: 0,
5410            formula_plane_mixed_topology_cache_builds: 0,
5411            formula_plane_mixed_topology_cache_hits: 0,
5412            formula_plane_mixed_topology_cache_overflows: 0,
5413            formula_plane_dirty_pending_events: 0,
5414            formula_plane_dirty_region_events_recorded: 0,
5415            formula_plane_dirty_span_region_events_recorded: 0,
5416            formula_plane_dirty_whole_span_seeds_recorded: 0,
5417            formula_plane_dirty_global_invalidations: 0,
5418            formula_plane_structural_span_candidates: 0,
5419            formula_plane_cycle_member_span_demotions: 0,
5420            formula_plane_array_result_span_demotions: 0,
5421            retained_scc_members: self.retained_scc_members.len(),
5422        }
5423    }
5424
5425    /// Read-only queries must not observe a partially staged request/action.
5426    pub(super) fn affected_closure_idle(&self) -> bool {
5427        self.active_evaluation_resource_request.is_none()
5428            && self.active_resource_ledger.is_none()
5429            && self.action_depth == 0
5430            && !self.freshness_armed()
5431            && !self.has_staged_formulas()
5432            && !self.graph.deferred_dirty_active()
5433    }
5434
5435    pub(super) fn affected_closure_owner(&self) -> &Arc<()> {
5436        &self.recalc_plan_token
5437    }
5438
5439    /// Mutation revision captured by read-only engine reports.
5440    pub(crate) fn inspection_mutation_revision(&self) -> u64 {
5441        self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
5442    }
5443
5444    #[cfg(test)]
5445    pub(crate) fn used_axis_bounds_cache_stats(&self) -> (usize, usize, usize, usize) {
5446        self.used_axis_bounds_cache
5447            .read()
5448            .ok()
5449            .and_then(|guard| {
5450                guard.as_ref().map(|cache| {
5451                    (
5452                        cache.row_hits.load(Ordering::Relaxed),
5453                        cache.row_misses.load(Ordering::Relaxed),
5454                        cache.col_hits.load(Ordering::Relaxed),
5455                        cache.col_misses.load(Ordering::Relaxed),
5456                    )
5457                })
5458            })
5459            .unwrap_or((0, 0, 0, 0))
5460    }
5461
5462    ///
5463    /// # Panics
5464    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
5465    pub fn set_first_load_assume_new(&mut self, enabled: bool) {
5466        self.assert_not_poisoned();
5467        self.graph.set_first_load_assume_new(enabled);
5468    }
5469
5470    pub fn first_load_assume_new(&self) -> bool {
5471        self.graph.first_load_assume_new()
5472    }
5473
5474    ///
5475    /// # Panics
5476    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
5477    pub fn reset_ensure_touched(&mut self) {
5478        self.assert_not_poisoned();
5479        self.graph.reset_ensure_touched();
5480    }
5481
5482    ///
5483    /// # Panics
5484    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
5485    pub fn finalize_sheet_index(&mut self, sheet: &str) {
5486        self.assert_not_poisoned();
5487        self.graph.finalize_sheet_index(sheet);
5488    }
5489
5490    /// Rebuild the requested sheet indexes in a single coordinate-map pass.
5491    #[doc(hidden)]
5492    ///
5493    /// # Panics
5494    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
5495    pub fn finalize_all_sheet_indexes<'a>(&mut self, sheets: impl IntoIterator<Item = &'a str>) {
5496        self.assert_not_poisoned();
5497        self.graph.finalize_all_sheet_indexes(sheets);
5498    }
5499
5500    /// Execute a named Engine action.
5501    ///
5502    /// Ticket 614 introduces this as the stable Engine-level transaction surface.
5503    /// For now actions are commit-only: they do not create changelog boundaries and they do not
5504    /// provide rollback/atomicity.
5505    ///
5506    /// Nested actions are deterministically handled by *disallowing* nesting: calling
5507    /// `Engine::action` while another action is active returns `EditorError::TransactionFailed`.
5508    pub fn action<T>(
5509        &mut self,
5510        name: impl AsRef<str>,
5511        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
5512    ) -> Result<T, crate::engine::EditorError> {
5513        self.ensure_not_poisoned()?;
5514        if self.action_depth != 0 {
5515            return Err(crate::engine::EditorError::TransactionFailed {
5516                reason: "Nested Engine::action calls are not supported (ticket 614: commit-only surface)"
5517                    .to_string(),
5518            });
5519        }
5520
5521        self.action_depth = 1;
5522        let engine_ptr: *mut Engine<R> = self;
5523        let _guard = ActionDepthGuard {
5524            engine: engine_ptr,
5525            _marker: std::marker::PhantomData,
5526        };
5527
5528        let mut tx = EngineAction {
5529            engine: self,
5530            name: name.as_ref().to_string(),
5531            capture: None,
5532            arrow_undo: None,
5533            atomic_policy: false,
5534            spill_projection_history: Default::default(),
5535        };
5536        f(&mut tx)
5537    }
5538
5539    /// Execute a named Engine action with atomic commit/rollback semantics.
5540    ///
5541    /// This variant does not require a `ChangeLog` and uses an internal journal for rollback.
5542    pub fn action_atomic<T>(
5543        &mut self,
5544        name: impl Into<String>,
5545        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
5546    ) -> Result<T, crate::engine::EditorError> {
5547        self.ensure_not_poisoned()?;
5548        let (v, _j) = self.action_atomic_journal(name, f)?;
5549        Ok(v)
5550    }
5551
5552    /// Like `action_atomic`, but returns the committed journal entry for undo/redo storage.
5553    pub fn action_atomic_journal<T>(
5554        &mut self,
5555        name: impl Into<String>,
5556        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
5557    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
5558        self.ensure_not_poisoned()?;
5559        if self.action_depth != 0 {
5560            return Err(crate::engine::EditorError::TransactionFailed {
5561                reason: "Nested Engine::action calls are not supported (deterministic rule)"
5562                    .to_string(),
5563            });
5564        }
5565
5566        self.action_depth = 1;
5567        let engine_ptr: *mut Engine<R> = self;
5568        let _guard = ActionDepthGuard {
5569            engine: engine_ptr,
5570            _marker: std::marker::PhantomData,
5571        };
5572
5573        let name_str = name.into();
5574        let mut capture = MutationCapture::new(Default::default());
5575        let start_len = capture.len();
5576        self.action_atomic_impl(&mut capture, start_len, true, name_str, f)
5577    }
5578
5579    fn action_atomic_impl<T>(
5580        &mut self,
5581        capture: &mut MutationCapture,
5582        start_len: usize,
5583        expand_runs: bool,
5584        name: String,
5585        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
5586    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
5587        let invalidation_baseline = self.invalidation_baseline();
5588        let mut arrow_undo = crate::engine::ArrowUndoBatch::default();
5589        let arrow_ptr: *mut crate::engine::ArrowUndoBatch = &mut arrow_undo;
5590
5591        let capture_ptr: *mut MutationCapture = capture;
5592        let mut tx = EngineAction {
5593            engine: self,
5594            name: name.clone(),
5595            capture: Some(capture_ptr),
5596            arrow_undo: Some(arrow_ptr),
5597            atomic_policy: true,
5598            spill_projection_history: Default::default(),
5599        };
5600
5601        let res = f(&mut tx);
5602
5603        // Capture graph structural delta for this action. The journal is a
5604        // public value: run records (Program 2) are expanded into it, except
5605        // for a caller that only publishes the capture to its change log
5606        // (`expand_runs` false), where the journal (plain events) drives
5607        // invalidation and records count as topology changes; a rollback
5608        // still replays the expanded events.
5609        let capture_ref = unsafe { &*capture_ptr };
5610        let has_runs = capture_ref.lazy_len() > 0;
5611        let graph_events: Vec<crate::engine::ChangeEvent> = if expand_runs || res.is_err() {
5612            capture_ref.expanded_events_from(start_len, 0)
5613        } else {
5614            capture_ref.events()[start_len..].to_vec()
5615        };
5616        let graph_batch = crate::engine::GraphUndoBatch {
5617            events: graph_events,
5618        };
5619        let affected_cells = arrow_undo.ops.len();
5620        let journal = crate::engine::ActionJournal {
5621            name,
5622            graph: graph_batch,
5623            arrow: arrow_undo,
5624            affected_cells,
5625        };
5626
5627        match res {
5628            Ok(v) => {
5629                if !journal.graph.is_empty() || !journal.arrow.is_empty() || has_runs {
5630                    for event in &journal.graph.events {
5631                        self.record_change_for_event(event);
5632                    }
5633                    let mut impact = Self::classify_change_events(
5634                        &journal.graph.events,
5635                        LoggedEditDirection::Original,
5636                    )
5637                    .max(Self::classify_arrow_undo(&journal.arrow));
5638                    if has_runs {
5639                        impact = impact.max(LoggedEditImpact::Topology);
5640                    }
5641                    self.apply_logged_edit_impact(impact, invalidation_baseline);
5642                }
5643                Ok((v, journal))
5644            }
5645            Err(e) => {
5646                if let Err(rb) = self.rollback_from_action_journal(&journal, invalidation_baseline)
5647                {
5648                    return Err(crate::engine::EditorError::TransactionFailed {
5649                        reason: format!(
5650                            "Engine::action_atomic rollback failed after error '{e}': {rb}"
5651                        ),
5652                    });
5653                }
5654                if !journal.graph.is_empty() || !journal.arrow.is_empty() {
5655                    for event in &journal.graph.events {
5656                        // Rollback restored projection ownership just like undo.
5657                        // Notify dependents without treating restored cells as
5658                        // new user writes that release that ownership again.
5659                        self.record_inverse_change_for_event(event);
5660                    }
5661                }
5662                Err(e)
5663            }
5664        }
5665    }
5666
5667    /// Execute a named Engine action, logging graph changes into the provided ChangeLog.
5668    ///
5669    /// Ticket 615: this variant provides atomicity. If the action returns an error, it rolls back:
5670    /// - Dependency graph structural edits (via inverse ChangeEvents)
5671    /// - Arrow-truth overlay writes mirrored from ChangeEvents
5672    /// - ChangeLog entries (published only after a successful commit)
5673    pub fn action_with_logger<T>(
5674        &mut self,
5675        log: &mut crate::engine::ChangeLog,
5676        name: impl AsRef<str>,
5677        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
5678    ) -> Result<T, crate::engine::EditorError> {
5679        self.ensure_not_poisoned()?;
5680        if self.action_depth != 0 {
5681            return Err(crate::engine::EditorError::TransactionFailed {
5682                reason: "Nested Engine::action calls are not supported (deterministic rule)"
5683                    .to_string(),
5684            });
5685        }
5686
5687        self.action_depth = 1;
5688        let engine_ptr: *mut Engine<R> = self;
5689        let _guard = ActionDepthGuard {
5690            engine: engine_ptr,
5691            _marker: std::marker::PhantomData,
5692        };
5693
5694        let name_str = name.as_ref().to_string();
5695        let mut capture = MutationCapture::new(log.current_meta());
5696        let start_len = capture.len();
5697        capture.begin_compound(name_str.clone());
5698
5699        // Mutation correctness uses the complete private capture. The provided ChangeLog remains
5700        // an observability sink and is not touched until the action outcome is known.
5701        let res = self.action_atomic_impl(&mut capture, start_len, false, name_str, f);
5702        capture.close_compounds();
5703
5704        match res {
5705            Ok((v, _journal)) => {
5706                log.publish_capture(capture);
5707                Ok(v)
5708            }
5709            Err(e) => {
5710                // Preserve sequence/group gaps without retaining failed events or evicting history.
5711                log.discard_capture(capture);
5712                Err(e)
5713            }
5714        }
5715    }
5716
5717    fn rollback_from_action_journal(
5718        &mut self,
5719        journal: &crate::engine::ActionJournal,
5720        invalidation_baseline: InvalidationBaseline,
5721    ) -> Result<(), crate::engine::EditorError> {
5722        // Invalidate first so a partial inverse failure cannot leave a changed
5723        // graph behind an apparently current schedule or lookup cache.
5724        self.invalidate_for_action_journal(
5725            journal,
5726            LoggedEditDirection::InverseReplay,
5727            invalidation_baseline,
5728        );
5729        // 1) Roll back the dependency graph structure.
5730        journal.graph.undo(&mut self.graph)?;
5731        // 2) Roll back engine row-visibility sidecar events.
5732        self.apply_inverse_row_visibility_events(&journal.graph.events);
5733        // 3) Roll back Arrow-truth overlays.
5734        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
5735        Ok(())
5736    }
5737
5738    fn rollback_from_change_events(
5739        &mut self,
5740        events: &[crate::engine::ChangeEvent],
5741        invalidation_baseline: InvalidationBaseline,
5742    ) -> Result<(), crate::engine::EditorError> {
5743        use crate::engine::ChangeEvent;
5744
5745        // Fail closed before applying inverses because replay can return after
5746        // only part of the batch has been restored.
5747        self.invalidate_for_change_events(
5748            events,
5749            LoggedEditDirection::InverseReplay,
5750            invalidation_baseline,
5751        );
5752
5753        // 1) Roll back the dependency graph.
5754        self.graph
5755            .authority_set_replay(crate::engine::authority::history::Replay::Undo);
5756        let rolled_back = (|| {
5757            let mut editor = crate::engine::VertexEditor::new(&mut self.graph);
5758            let mut compound_stack: Vec<usize> = Vec::new();
5759            for (i, ev) in events.iter().enumerate().rev() {
5760                match ev {
5761                    ChangeEvent::CompoundEnd { depth } => {
5762                        compound_stack.push(*depth);
5763                        if let Some(description) =
5764                            crate::engine::graph::editor::change_log::compound_start_description(
5765                                i,
5766                                |j| &events[j],
5767                            )
5768                        {
5769                            editor.inverse_compound_end(description);
5770                        }
5771                    }
5772                    ChangeEvent::CompoundStart { depth, .. } => {
5773                        if compound_stack.last() == Some(depth) {
5774                            compound_stack.pop();
5775                        }
5776                        editor.apply_inverse(ev.clone())?;
5777                    }
5778                    ChangeEvent::SetRowVisibility { .. } => {
5779                        // Engine-side metadata handled after dropping graph editor borrow.
5780                    }
5781                    _ => {
5782                        editor.apply_inverse(ev.clone())?;
5783                    }
5784                }
5785            }
5786            Ok::<_, crate::engine::EditorError>(())
5787        })();
5788        self.graph
5789            .authority_set_replay(crate::engine::authority::history::Replay::Forward);
5790        rolled_back?;
5791
5792        // 2) Roll back engine row-visibility metadata.
5793        for ev in events.iter().rev() {
5794            self.apply_inverse_row_visibility_event(ev);
5795        }
5796
5797        // 3) Roll back Arrow-truth overlays mirrored from those ChangeEvents.
5798        for ev in events.iter().rev() {
5799            self.mirror_inverse_change_to_arrow(ev);
5800        }
5801
5802        Ok(())
5803    }
5804
5805    fn read_cell_formula_ast(&self, sheet: &str, row: u32, col: u32) -> Option<ASTNode> {
5806        let sheet_id = self.graph.sheet_id(sheet)?;
5807        let coord = Coord::from_excel(row, col, true, true);
5808        let cell = CellRef::new(sheet_id, coord);
5809        let vid = self.graph.get_vertex_for_cell(&cell)?;
5810        self.graph.get_formula(vid)
5811    }
5812
5813    pub fn define_name_with_logger(
5814        &mut self,
5815        log: &mut crate::engine::ChangeLog,
5816        name: &str,
5817        definition: NamedDefinition,
5818        scope: NameScope,
5819    ) -> Result<(), crate::engine::EditorError> {
5820        self.ensure_not_poisoned()?;
5821        self.graph
5822            .validate_define_name(name, scope)
5823            .map_err(crate::engine::EditorError::Excel)?;
5824
5825        {
5826            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
5827            editor.define_name(name, definition, scope)?;
5828        }
5829        self.record_structural_change(StructuralScope::AllSheets);
5830
5831        self.mark_topology_edited();
5832
5833        Ok(())
5834    }
5835
5836    pub fn update_name_with_logger(
5837        &mut self,
5838        log: &mut crate::engine::ChangeLog,
5839        name: &str,
5840        definition: NamedDefinition,
5841        scope: NameScope,
5842    ) -> Result<(), crate::engine::EditorError> {
5843        self.ensure_not_poisoned()?;
5844        self.graph
5845            .validate_existing_name(name, scope)
5846            .map_err(crate::engine::EditorError::Excel)?;
5847        {
5848            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
5849            editor.update_name(name, definition, scope)?;
5850        }
5851        self.record_structural_change(StructuralScope::AllSheets);
5852
5853        self.mark_topology_edited();
5854
5855        Ok(())
5856    }
5857
5858    pub fn delete_name_with_logger(
5859        &mut self,
5860        log: &mut crate::engine::ChangeLog,
5861        name: &str,
5862        scope: NameScope,
5863    ) -> Result<(), crate::engine::EditorError> {
5864        self.ensure_not_poisoned()?;
5865        self.graph
5866            .validate_existing_name(name, scope)
5867            .map_err(crate::engine::EditorError::Excel)?;
5868        {
5869            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
5870            editor.delete_name(name, scope)?;
5871        }
5872        self.record_structural_change(StructuralScope::AllSheets);
5873
5874        self.mark_topology_edited();
5875
5876        Ok(())
5877    }
5878
5879    pub fn edit_with_logger<T>(
5880        &mut self,
5881        log: &mut crate::engine::ChangeLog,
5882        f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
5883    ) -> Result<T, crate::engine::EditorError> {
5884        self.ensure_not_poisoned()?;
5885        let mut capture = MutationCapture::new(log.current_meta());
5886        let result = self.edit_with_capture(&mut capture, f);
5887        capture.close_compounds();
5888        match result {
5889            Ok(value) => {
5890                log.publish_capture(capture);
5891                Ok(value)
5892            }
5893            Err(error) => {
5894                log.discard_capture(capture);
5895                Err(error)
5896            }
5897        }
5898    }
5899
5900    fn edit_with_capture<T>(
5901        &mut self,
5902        capture: &mut MutationCapture,
5903        f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
5904    ) -> Result<T, crate::engine::EditorError> {
5905        let invalidation_baseline = self.invalidation_baseline();
5906        let start_len = capture.len();
5907        let lazy_start = capture.lazy_len();
5908
5909        // Provide a spill snapshot reader so VertexEditor can snapshot Arrow-truth spill values
5910        // (graph value cache is intentionally empty in canonical mode).
5911        struct ArrowSpillReader<'a> {
5912            sheets: &'a crate::arrow_store::SheetStore,
5913        }
5914        impl crate::engine::graph::editor::vertex_editor::SpillValueReader for ArrowSpillReader<'_> {
5915            fn read_cell_value(
5916                &self,
5917                sheet: &str,
5918                row: u32,
5919                col: u32,
5920            ) -> Option<formualizer_common::LiteralValue> {
5921                use formualizer_common::LiteralValue;
5922                let asheet = self.sheets.sheet(sheet)?;
5923                let r0 = row.saturating_sub(1) as usize;
5924                let c0 = col.saturating_sub(1) as usize;
5925                let v = asheet.get_cell_value(r0, c0);
5926                if matches!(v, LiteralValue::Empty) {
5927                    None
5928                } else {
5929                    Some(v)
5930                }
5931            }
5932        }
5933
5934        let ret = {
5935            let spill_reader = ArrowSpillReader {
5936                sheets: &self.arrow_sheets,
5937            };
5938            let mut editor = crate::engine::VertexEditor::with_capture_and_spill_reader(
5939                &mut self.graph,
5940                capture,
5941                &spill_reader,
5942            );
5943            f(&mut editor)
5944        };
5945
5946        // Plain events only: run records (Program 2) stand for
5947        // `FormulaAdjusted` events, which have no forward effect here but
5948        // topology invalidation.
5949        let new_events = capture.events()[start_len..].to_vec();
5950        let new_runs = capture.lazy_len() > lazy_start;
5951        if new_events.iter().any(|event| {
5952            matches!(
5953                event,
5954                ChangeEvent::DefineName { .. }
5955                    | ChangeEvent::UpdateName { .. }
5956                    | ChangeEvent::DeleteName { .. }
5957            )
5958        }) {
5959            let all = capture.expanded_events_from(start_len, lazy_start);
5960            self.rollback_from_change_events(&all, invalidation_baseline)?;
5961            return Err(crate::engine::EditorError::TransactionUnsupported {
5962                reason: "name mutations must use Engine's prepared logged-name APIs".to_string(),
5963            });
5964        }
5965
5966        // Mirror value-impacting graph events to Arrow for forward edits.
5967        // This keeps Arrow overlays (delta + computed) consistent when edits clear/commit spills.
5968        self.clear_logged_cell_format_states(&new_events);
5969        for ev in &new_events {
5970            self.mirror_forward_change_to_arrow(ev);
5971        }
5972        for ev in &new_events {
5973            self.record_change_for_event(ev);
5974        }
5975
5976        // Atomic EngineAction calls publish one invalidation for their complete
5977        // journal at commit/rollback. Direct logged edits publish here.
5978        if self.action_depth == 0 {
5979            let mut impact =
5980                Self::classify_change_events(&new_events, LoggedEditDirection::Original);
5981            if new_runs {
5982                impact = impact.max(LoggedEditImpact::Topology);
5983            }
5984            self.apply_logged_edit_impact(impact, invalidation_baseline);
5985        }
5986
5987        Ok(ret)
5988    }
5989
5990    pub(crate) fn preflight_replay_admission(
5991        &mut self,
5992        events: &[ChangeEvent],
5993        forward: bool,
5994    ) -> Result<(), crate::engine::EditorError> {
5995        if !self.graph_admission_enabled() {
5996            return Ok(());
5997        }
5998        let mut vertex_delta = 0i128;
5999        let mut edge_delta = 0i128;
6000        let mut added_vertices = 0usize;
6001        let mut added_edges = 0usize;
6002        let mut formula_cells = BTreeSet::new();
6003        for event in events {
6004            match event {
6005                ChangeEvent::AddVertex {
6006                    formula,
6007                    coord,
6008                    sheet_id,
6009                    ..
6010                } => {
6011                    let delta = if forward { 1 } else { -1 };
6012                    vertex_delta += delta;
6013                    if forward {
6014                        added_vertices = added_vertices.saturating_add(1);
6015                        if formula.is_some() {
6016                            formula_cells.insert((*sheet_id, coord.row(), coord.col()));
6017                        }
6018                    }
6019                }
6020                ChangeEvent::RemoveVertex {
6021                    old_formula,
6022                    coord,
6023                    sheet_id,
6024                    ..
6025                } => {
6026                    let delta = if forward { -1 } else { 1 };
6027                    vertex_delta += delta;
6028                    if !forward {
6029                        added_vertices = added_vertices.saturating_add(1);
6030                        if old_formula.is_some()
6031                            && let (Some(sheet_id), Some(coord)) = (sheet_id, coord)
6032                        {
6033                            formula_cells.insert((*sheet_id, coord.row(), coord.col()));
6034                        }
6035                    }
6036                }
6037                ChangeEvent::EdgeAdded { .. } => {
6038                    let delta = if forward { 1 } else { -1 };
6039                    edge_delta += delta;
6040                    if forward {
6041                        added_edges = added_edges.saturating_add(1);
6042                    }
6043                }
6044                ChangeEvent::EdgeRemoved { .. } => {
6045                    let delta = if forward { -1 } else { 1 };
6046                    edge_delta += delta;
6047                    if !forward {
6048                        added_edges = added_edges.saturating_add(1);
6049                    }
6050                }
6051                ChangeEvent::SetFormula {
6052                    addr, old_formula, ..
6053                } if (forward || old_formula.is_some()) => {
6054                    formula_cells.insert((addr.sheet_id, addr.coord.row(), addr.coord.col()));
6055                }
6056                _ => {}
6057            }
6058        }
6059        let stats = self.graph.baseline_stats();
6060        let final_vertices = i128::try_from(stats.graph_vertex_count)
6061            .ok()
6062            .and_then(|count| count.checked_add(vertex_delta))
6063            .and_then(|count| usize::try_from(count).ok())
6064            .ok_or_else(|| {
6065                crate::engine::EditorError::Excel(
6066                    ExcelError::new(ExcelErrorKind::NImpl)
6067                        .with_message("replay vertex count overflow"),
6068                )
6069            })?;
6070        let final_edges = i128::try_from(stats.graph_edge_count)
6071            .ok()
6072            .and_then(|count| count.checked_add(edge_delta))
6073            .and_then(|count| usize::try_from(count).ok())
6074            .ok_or_else(|| {
6075                crate::engine::EditorError::Excel(
6076                    ExcelError::new(ExcelErrorKind::NImpl)
6077                        .with_message("replay edge count overflow"),
6078                )
6079            })?;
6080        self.preflight_graph_admission(crate::engine::resource_ledger::GraphAdmission {
6081            final_vertices,
6082            final_edges,
6083            materialization_cells: formula_cells.len() as u64,
6084            added_vertices,
6085            added_edges,
6086        })
6087        .map_err(crate::engine::EditorError::Excel)
6088    }
6089
6090    /// Undo the last group still retained by the provided audit log.
6091    ///
6092    /// Disabled, zero-cap, and evicted history is unavailable on this index-based path. Use an
6093    /// explicit `ActionJournal` with `undo_action` when undo must be independent of audit retention.
6094    pub fn undo_logged(
6095        &mut self,
6096        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
6097        log: &mut crate::engine::ChangeLog,
6098    ) -> Result<(), crate::engine::EditorError> {
6099        self.ensure_not_poisoned()?;
6100        let pending_events = log
6101            .last_group_indices()
6102            .into_iter()
6103            .map(|index| log.events()[index].clone())
6104            .collect::<Vec<_>>();
6105        self.preflight_replay_admission(&pending_events, false)?;
6106        let invalidation_baseline = self.invalidation_baseline();
6107        // UndoEngine can fail after partially applying the batch, so publish
6108        // invalidation before replay rather than only on the success path.
6109        self.invalidate_for_change_events(
6110            &pending_events,
6111            LoggedEditDirection::InverseReplay,
6112            invalidation_baseline,
6113        );
6114        let batch = undo.undo(&mut self.graph, log)?;
6115        for item in batch.iter().rev() {
6116            self.apply_inverse_row_visibility_event(&item.event);
6117            self.apply_inverse_staged_formula_event(&item.event);
6118        }
6119        if !batch.is_empty() {
6120            let events = batch
6121                .iter()
6122                .map(|item| item.event.clone())
6123                .collect::<Vec<_>>();
6124            self.clear_logged_cell_format_states(&events);
6125        }
6126        self.mirror_undo_batch_to_arrow(&batch);
6127        if !batch.is_empty() {
6128            for item in &batch {
6129                self.record_inverse_change_for_event(&item.event);
6130            }
6131        }
6132        crate::engine::trace::fz_event!(
6133            tracing::Level::INFO,
6134            "history",
6135            "history.replay",
6136            op = "undo",
6137            events_replayed = batch.len(),
6138            ownership_resyncs = batch.len()
6139        );
6140        Ok(())
6141    }
6142
6143    pub fn redo_logged(
6144        &mut self,
6145        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
6146        log: &mut crate::engine::ChangeLog,
6147    ) -> Result<(), crate::engine::EditorError> {
6148        self.ensure_not_poisoned()?;
6149        let pending_events = undo.pending_redo_events();
6150        self.preflight_replay_admission(&pending_events, true)?;
6151        let invalidation_baseline = self.invalidation_baseline();
6152        self.invalidate_for_change_events(
6153            &pending_events,
6154            LoggedEditDirection::ForwardReplay,
6155            invalidation_baseline,
6156        );
6157        let batch = undo.redo(&mut self.graph, log)?;
6158        for item in &batch {
6159            self.apply_forward_row_visibility_event(&item.event);
6160            self.apply_forward_staged_formula_event(&item.event);
6161        }
6162        if !batch.is_empty() {
6163            let events = batch
6164                .iter()
6165                .map(|item| item.event.clone())
6166                .collect::<Vec<_>>();
6167            self.clear_logged_cell_format_states(&events);
6168        }
6169        self.mirror_redo_batch_to_arrow(&batch);
6170        if !batch.is_empty() {
6171            for item in &batch {
6172                self.record_change_for_event(&item.event);
6173            }
6174        }
6175        crate::engine::trace::fz_event!(
6176            tracing::Level::INFO,
6177            "history",
6178            "history.replay",
6179            op = "redo",
6180            events_replayed = batch.len(),
6181            ownership_resyncs = batch.len()
6182        );
6183        Ok(())
6184    }
6185
6186    /// Undo the last committed atomic action using the journal stack.
6187    ///
6188    /// This path does not require a `ChangeLog`.
6189    pub fn undo_action(
6190        &mut self,
6191        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
6192    ) -> Result<(), crate::engine::EditorError> {
6193        self.ensure_not_poisoned()?;
6194        let Some(journal) = undo.pop_undo_action() else {
6195            return Ok(());
6196        };
6197        if let Err(error) = self.preflight_replay_admission(&journal.graph.events, false) {
6198            undo.push_done_action(journal);
6199            return Err(error);
6200        }
6201        let invalidation_baseline = self.invalidation_baseline();
6202
6203        self.invalidate_for_action_journal(
6204            &journal,
6205            LoggedEditDirection::InverseReplay,
6206            invalidation_baseline,
6207        );
6208        journal.graph.undo(&mut self.graph)?;
6209        self.apply_inverse_row_visibility_events(&journal.graph.events);
6210        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
6211        if !journal.graph.is_empty() || !journal.arrow.is_empty() {
6212            for event in &journal.graph.events {
6213                self.record_inverse_change_for_event(event);
6214            }
6215        }
6216
6217        #[cfg(feature = "tracing")]
6218        let events_replayed = journal.graph.events.len();
6219        crate::engine::trace::fz_event!(
6220            tracing::Level::INFO,
6221            "history",
6222            "history.replay",
6223            op = "undo",
6224            events_replayed,
6225            ownership_resyncs = events_replayed
6226        );
6227        undo.push_redo_action(journal);
6228        Ok(())
6229    }
6230
6231    /// Redo the last undone atomic action using the journal stack.
6232    ///
6233    /// This path does not require a `ChangeLog`.
6234    pub fn redo_action(
6235        &mut self,
6236        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
6237    ) -> Result<(), crate::engine::EditorError> {
6238        self.ensure_not_poisoned()?;
6239        let Some(journal) = undo.pop_redo_action() else {
6240            return Ok(());
6241        };
6242        if let Err(error) = self.preflight_replay_admission(&journal.graph.events, true) {
6243            undo.push_redo_action(journal);
6244            return Err(error);
6245        }
6246        let invalidation_baseline = self.invalidation_baseline();
6247        self.invalidate_for_action_journal(
6248            &journal,
6249            LoggedEditDirection::ForwardReplay,
6250            invalidation_baseline,
6251        );
6252        journal.graph.redo(&mut self.graph)?;
6253        self.apply_forward_row_visibility_events(&journal.graph.events);
6254        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ false);
6255        if !journal.graph.is_empty() || !journal.arrow.is_empty() {
6256            for event in &journal.graph.events {
6257                self.record_change_for_event(event);
6258            }
6259        }
6260
6261        #[cfg(feature = "tracing")]
6262        let events_replayed = journal.graph.events.len();
6263        crate::engine::trace::fz_event!(
6264            tracing::Level::INFO,
6265            "history",
6266            "history.replay",
6267            op = "redo",
6268            events_replayed,
6269            ownership_resyncs = events_replayed
6270        );
6271        undo.push_done_action(journal);
6272        Ok(())
6273    }
6274
6275    fn cellref_to_sheet_row_col(&self, addr: &crate::reference::CellRef) -> (String, u32, u32) {
6276        let sheet = self.graph.sheet_name(addr.sheet_id).to_string();
6277        // Coord stores 0-based indices.
6278        let row = addr.coord.row() + 1;
6279        let col = addr.coord.col() + 1;
6280        (sheet, row, col)
6281    }
6282
6283    fn mirror_undo_batch_to_arrow(
6284        &mut self,
6285        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
6286    ) {
6287        // Undo applies inverses in reverse order.
6288        for item in batch.iter().rev() {
6289            self.mirror_inverse_change_to_arrow(&item.event);
6290        }
6291    }
6292
6293    fn mirror_redo_batch_to_arrow(
6294        &mut self,
6295        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
6296    ) {
6297        // Redo applies events in forward order.
6298        for item in batch.iter() {
6299            self.mirror_forward_change_to_arrow(&item.event);
6300        }
6301    }
6302
6303    fn mirror_inverse_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
6304        use crate::engine::ChangeEvent;
6305        use formualizer_common::LiteralValue;
6306
6307        match ev {
6308            ChangeEvent::SetValue {
6309                addr,
6310                old_value,
6311                old_formula,
6312                ..
6313            } => {
6314                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
6315                if old_formula.is_some() {
6316                    self.clear_delta_overlay_cell(&sheet, row, col);
6317                } else {
6318                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
6319                    self.mirror_value_to_overlay(&sheet, row, col, &v);
6320                }
6321            }
6322            ChangeEvent::SetFormula {
6323                addr,
6324                old_value,
6325                old_formula,
6326                ..
6327            } => {
6328                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
6329                if old_formula.is_some() {
6330                    self.clear_delta_overlay_cell(&sheet, row, col);
6331                } else {
6332                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
6333                    self.mirror_value_to_overlay(&sheet, row, col, &v);
6334                }
6335            }
6336            ChangeEvent::SpillCommitted { old, new, .. } => {
6337                // Inverse: restore `old` (or clear if none).
6338                self.mirror_spill_snapshot(new, /*clear_only=*/ true);
6339                if let Some(snap) = old {
6340                    self.mirror_spill_snapshot(snap, /*clear_only=*/ false);
6341                }
6342            }
6343            ChangeEvent::SpillCleared { old, .. } => {
6344                // Inverse: restore prior spill.
6345                self.mirror_spill_snapshot(old, /*clear_only=*/ false);
6346            }
6347            ChangeEvent::SetRowVisibility { .. } => {
6348                // Engine-side metadata only; no Arrow overlay effect.
6349            }
6350            _ => {}
6351        }
6352    }
6353
6354    fn mirror_forward_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
6355        use crate::engine::ChangeEvent;
6356
6357        match ev {
6358            ChangeEvent::SetValue { addr, new, .. } => {
6359                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
6360                self.mirror_value_to_overlay(&sheet, row, col, new);
6361            }
6362            ChangeEvent::SetFormula { addr, .. } => {
6363                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
6364                self.clear_delta_overlay_cell(&sheet, row, col);
6365                // Keep any computed overlay for this cell as-is; it will be recomputed on demand.
6366            }
6367            ChangeEvent::SpillCommitted { old, new, .. } => {
6368                if let Some(snap) = old {
6369                    self.mirror_spill_snapshot(snap, /*clear_only=*/ true);
6370                }
6371                self.mirror_spill_snapshot(new, /*clear_only=*/ false);
6372            }
6373            ChangeEvent::SpillCleared { old, .. } => {
6374                self.mirror_spill_snapshot(old, /*clear_only=*/ true);
6375            }
6376            ChangeEvent::SetRowVisibility { .. } => {
6377                // Engine-side metadata only; no Arrow overlay effect.
6378            }
6379            _ => {
6380                // Other graph structural operations do not have direct value effects in Arrow.
6381            }
6382        }
6383    }
6384
6385    fn mirror_spill_snapshot(
6386        &mut self,
6387        snap: &crate::engine::graph::editor::change_log::SpillSnapshot,
6388        clear_only: bool,
6389    ) {
6390        use formualizer_common::LiteralValue;
6391
6392        let Some(first) = snap.target_cells.first() else {
6393            return;
6394        };
6395        for cell in &snap.target_cells {
6396            let r_off = cell.coord.row().saturating_sub(first.coord.row()) as usize;
6397            let c_off = cell.coord.col().saturating_sub(first.coord.col()) as usize;
6398            let Some(v) = snap.values.get(r_off).and_then(|row| row.get(c_off)) else {
6399                continue;
6400            };
6401            let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
6402            // Owned snapshot cells can live in compacted base lanes with no
6403            // computed entry. They still need an Empty mask on clear; otherwise
6404            // a repeated edit sees orphan values and blocks the next spill.
6405            if clear_only && cell != first && self.read_cell_formula_ast(&sheet, r, c).is_some() {
6406                continue;
6407            }
6408            let out = if clear_only {
6409                LiteralValue::Empty
6410            } else {
6411                v.clone()
6412            };
6413            self.mirror_value_to_computed_overlay(&sheet, r, c, &out);
6414        }
6415    }
6416
6417    ///
6418    /// # Panics
6419    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6420    pub fn set_default_sheet_by_name(&mut self, name: &str) {
6421        self.assert_not_poisoned();
6422        self.graph.set_default_sheet_by_name(name);
6423    }
6424
6425    ///
6426    /// # Panics
6427    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6428    pub fn set_default_sheet_by_id(&mut self, id: SheetId) {
6429        self.assert_not_poisoned();
6430        self.graph.set_default_sheet_by_id(id);
6431    }
6432
6433    ///
6434    /// # Panics
6435    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6436    pub fn set_sheet_index_mode(&mut self, mode: crate::engine::SheetIndexMode) {
6437        self.assert_not_poisoned();
6438        self.graph.set_sheet_index_mode(mode);
6439    }
6440
6441    #[cfg(feature = "test-support")]
6442    #[doc(hidden)]
6443    ///
6444    /// # Panics
6445    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6446    pub fn mark_all_formulas_dirty_for_test(&mut self) {
6447        self.assert_not_poisoned();
6448        self.mark_all_formula_vertices_dirty();
6449    }
6450
6451    #[cfg(feature = "test-support")]
6452    #[doc(hidden)]
6453    pub fn take_criteria_mask_work_for_test() -> (usize, usize) {
6454        criteria_mask_test_hooks::take_mask_work()
6455    }
6456
6457    #[cfg(feature = "test-support")]
6458    #[doc(hidden)]
6459    pub fn lookup_index_cache_report_for_test(&self) -> LookupIndexCacheReport {
6460        self.lookup_index_cache.report()
6461    }
6462
6463    #[cfg(any(test, feature = "benchmark_internal"))]
6464    #[doc(hidden)]
6465    ///
6466    /// # Panics
6467    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6468    pub fn reset_recalc_reuse_probe(&mut self) {
6469        self.assert_not_poisoned();
6470        *self.recalc_reuse_probe.get_mut().unwrap() = RecalcReuseProbe::default();
6471    }
6472
6473    #[cfg(any(test, feature = "benchmark_internal"))]
6474    #[doc(hidden)]
6475    pub fn recalc_reuse_probe(&self) -> RecalcReuseProbe {
6476        let mut probe = self.recalc_reuse_probe.lock().unwrap().clone();
6477        if let Some(cached) = self.cached_static_schedule.as_ref() {
6478            let entry_bytes = |e: &CachedScheduleEntry| {
6479                std::mem::size_of::<CachedScheduleEntry>()
6480                    + e.candidate_vertices.heap_bytes()
6481                    + std::mem::size_of::<crate::engine::Schedule>()
6482                    + 2 * std::mem::size_of::<usize>()
6483                    + schedule_probe_retained_bytes(&e.schedule)
6484            };
6485            probe.schedule_retained_bytes = entry_bytes(cached)
6486                + self.recent_schedules.iter().map(entry_bytes).sum::<usize>()
6487                + self.base_schedule.as_ref().map_or(0, entry_bytes)
6488                + self.recent_schedules.capacity() * std::mem::size_of::<CachedScheduleEntry>();
6489        }
6490        probe
6491    }
6492
6493    #[cfg(test)]
6494    pub(crate) fn cached_static_schedule_for_test(&self) -> Option<Arc<crate::engine::Schedule>> {
6495        self.cached_static_schedule
6496            .as_ref()
6497            .map(|cached| Arc::clone(&cached.schedule))
6498    }
6499
6500    fn clear_cached_static_schedule(&mut self) {
6501        self.cached_static_schedule = None;
6502        self.recent_schedules.clear();
6503        self.base_schedule = None;
6504    }
6505
6506    /// Keep a replaced schedule among the recent ones when it is still
6507    /// current and small; drop stale ones and the oldest beyond the bounds.
6508    fn retain_recent_schedule(&mut self, entry: CachedScheduleEntry) {
6509        const RECENT_SCHEDULES: usize = 8;
6510        const RECENT_SCHEDULE_VERTICES: usize = 65_536;
6511        let revision = self.schedule_cache_authority_revision();
6512        let epoch = self.topology_epoch;
6513        self.recent_schedules
6514            .retain(|e| e.topology_epoch == epoch && e.authority_revision == revision);
6515        let current =
6516            |e: &CachedScheduleEntry| e.topology_epoch == epoch && e.authority_revision == revision;
6517        if self.base_schedule.as_ref().is_some_and(|b| !current(b)) {
6518            self.base_schedule = None;
6519        }
6520        // The largest current schedule becomes the base; a replaced base
6521        // may still join the recent ones.
6522        let mut entry = entry;
6523        if current(&entry)
6524            && entry.candidate_vertices.len() > BASE_SCHEDULE_MIN_VERTICES
6525            && self
6526                .base_schedule
6527                .as_ref()
6528                .is_none_or(|b| entry.candidate_vertices.len() > b.candidate_vertices.len())
6529        {
6530            match self.base_schedule.replace(entry) {
6531                Some(previous) => entry = previous,
6532                None => return,
6533            }
6534        }
6535        if entry.topology_epoch != epoch
6536            || entry.authority_revision != revision
6537            || entry.candidate_vertices.len() > RECENT_SCHEDULE_VERTICES / 4
6538        {
6539            return;
6540        }
6541        self.recent_schedules.push(entry);
6542        let mut total: usize = self
6543            .recent_schedules
6544            .iter()
6545            .map(|e| e.candidate_vertices.len())
6546            .sum();
6547        while self.recent_schedules.len() > RECENT_SCHEDULES || total > RECENT_SCHEDULE_VERTICES {
6548            let oldest = self.recent_schedules.remove(0);
6549            total -= oldest.candidate_vertices.len();
6550        }
6551    }
6552
6553    fn invalidation_baseline(&self) -> InvalidationBaseline {
6554        InvalidationBaseline {
6555            snapshot_id: self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed),
6556            topology_epoch: self.topology_epoch,
6557        }
6558    }
6559
6560    fn classify_change_events(
6561        events: &[crate::engine::ChangeEvent],
6562        direction: LoggedEditDirection,
6563    ) -> LoggedEditImpact {
6564        use crate::engine::ChangeEvent;
6565
6566        events
6567            .iter()
6568            .map(|event| match event {
6569                ChangeEvent::CompoundStart { .. } | ChangeEvent::CompoundEnd { .. } => {
6570                    LoggedEditImpact::NoOp
6571                }
6572                ChangeEvent::SetRowVisibility { .. }
6573                | ChangeEvent::SetValue {
6574                    old_formula: None,
6575                    old_value: Some(_),
6576                    ..
6577                } => LoggedEditImpact::DataOnly,
6578                // Original canonical writes can lack an Arrow old value while
6579                // updating an existing placeholder. Inverse replay actually
6580                // removes that vertex; a later redo recreates it.
6581                ChangeEvent::SetValue {
6582                    old_formula: None,
6583                    old_value: None,
6584                    ..
6585                } if direction == LoggedEditDirection::Original => LoggedEditImpact::DataOnly,
6586                ChangeEvent::SetValue { .. }
6587                | ChangeEvent::SetFormula { .. }
6588                | ChangeEvent::AddVertex { .. }
6589                | ChangeEvent::RemoveVertex { .. }
6590                | ChangeEvent::VertexMoved { .. }
6591                | ChangeEvent::FormulaAdjusted { .. }
6592                | ChangeEvent::NamedRangeAdjusted { .. }
6593                | ChangeEvent::TableResized { .. }
6594                | ChangeEvent::EdgeAdded { .. }
6595                | ChangeEvent::EdgeRemoved { .. }
6596                | ChangeEvent::DefineName { .. }
6597                | ChangeEvent::UpdateName { .. }
6598                | ChangeEvent::DeleteName { .. }
6599                | ChangeEvent::SpillCommitted { .. }
6600                | ChangeEvent::SpillCleared { .. }
6601                | ChangeEvent::StagedFormulaCellChanged { .. } => LoggedEditImpact::Topology,
6602            })
6603            .max()
6604            .unwrap_or(LoggedEditImpact::NoOp)
6605    }
6606
6607    fn classify_arrow_undo(arrow: &crate::engine::ArrowUndoBatch) -> LoggedEditImpact {
6608        use crate::engine::ArrowOp;
6609
6610        arrow
6611            .ops
6612            .iter()
6613            .map(|op| match op {
6614                ArrowOp::SetDeltaCell { .. } | ArrowOp::SetComputedCell { .. } => {
6615                    LoggedEditImpact::DataOnly
6616                }
6617                ArrowOp::RestoreComputedRect { .. }
6618                | ArrowOp::InsertRows { .. }
6619                | ArrowOp::InsertCols { .. } => LoggedEditImpact::Topology,
6620            })
6621            .max()
6622            .unwrap_or(LoggedEditImpact::NoOp)
6623    }
6624
6625    fn apply_logged_edit_impact(
6626        &mut self,
6627        impact: LoggedEditImpact,
6628        baseline: InvalidationBaseline,
6629    ) {
6630        match impact {
6631            LoggedEditImpact::NoOp => {}
6632            LoggedEditImpact::DataOnly => {
6633                if self.topology_epoch == baseline.topology_epoch
6634                    && self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
6635                        == baseline.snapshot_id
6636                {
6637                    self.mark_data_edited();
6638                }
6639            }
6640            LoggedEditImpact::Topology => {
6641                // Some structural entry points already publish topology
6642                // invalidation. Do not bump the same batch twice.
6643                if self.topology_epoch == baseline.topology_epoch {
6644                    self.mark_topology_edited();
6645                }
6646            }
6647        }
6648    }
6649
6650    fn invalidate_for_change_events(
6651        &mut self,
6652        events: &[crate::engine::ChangeEvent],
6653        direction: LoggedEditDirection,
6654        baseline: InvalidationBaseline,
6655    ) {
6656        self.apply_logged_edit_impact(Self::classify_change_events(events, direction), baseline);
6657    }
6658
6659    fn invalidate_for_action_journal(
6660        &mut self,
6661        journal: &crate::engine::ActionJournal,
6662        direction: LoggedEditDirection,
6663        baseline: InvalidationBaseline,
6664    ) {
6665        let impact = Self::classify_change_events(&journal.graph.events, direction)
6666            .max(Self::classify_arrow_undo(&journal.arrow));
6667        self.apply_logged_edit_impact(impact, baseline);
6668    }
6669
6670    /// Mark data edited: bump snapshot and set edited flag.
6671    /// Value-only edits keep the stable-topology schedule cache alive.
6672    ///
6673    /// # Panics
6674    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6675    pub fn mark_data_edited(&mut self) {
6676        self.assert_not_poisoned();
6677        self.lookup_index_cache.clear();
6678        self.snapshot_id
6679            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6680        self.has_edited = true;
6681    }
6682
6683    /// Mark a topology-changing edit: bump snapshot + topology epoch and invalidate cached schedules.
6684    ///
6685    /// # Panics
6686    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6687    pub fn mark_topology_edited(&mut self) {
6688        self.assert_not_poisoned();
6689        self.arrow_sheet_index.take();
6690        self.lookup_index_cache.clear();
6691        self.snapshot_id
6692            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
6693        self.topology_epoch = self.topology_epoch.wrapping_add(1);
6694        self.graph.bump_topology_revision();
6695        self.clear_cached_static_schedule();
6696        if let Some(ledger) = self.active_resource_ledger.as_mut() {
6697            let released = ledger.account_mixed_cache(0);
6698            debug_assert!(released.is_ok());
6699        }
6700        self.has_edited = true;
6701        // Eager sync at a topology edit, so read-only (`&self`) plans and
6702        // inspection see a current authority (not during a load or an open
6703        // structural capture).
6704        self.graph.authority_sync_eager();
6705    }
6706
6707    fn mark_all_formula_vertices_dirty(&mut self) {
6708        let vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
6709        for vertex in vertices {
6710            self.graph.mark_vertex_dirty(vertex);
6711        }
6712    }
6713
6714    fn mark_moved_formula_vertices_dirty(
6715        &mut self,
6716        summary: &crate::engine::graph::editor::vertex_editor::ShiftSummary,
6717    ) {
6718        for vertex in &summary.vertices_moved {
6719            if self.graph.has_formula(*vertex) {
6720                self.graph.mark_vertex_dirty(*vertex);
6721            }
6722        }
6723    }
6724
6725    /// Access Arrow sheet store (read-only)
6726    pub fn sheet_store(&self) -> &SheetStore {
6727        &self.arrow_sheets
6728    }
6729
6730    /// True when any sheet carries manual/filter row-visibility state.
6731    /// Used by load-time freshness checks (see `Engine::adopt_file_sheets`).
6732    pub(crate) fn has_row_visibility_state(&self) -> bool {
6733        !self.row_visibility.is_empty()
6734    }
6735
6736    /// Access Arrow sheet store (mutable)
6737    ///
6738    /// # Panics
6739    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6740    pub fn sheet_store_mut(&mut self) -> &mut SheetStore {
6741        self.assert_not_poisoned();
6742        self.arrow_sheet_index.take();
6743        &mut self.arrow_sheets
6744    }
6745
6746    pub fn has_staged_formulas(&self) -> bool {
6747        !self.staged_formulas.is_empty()
6748    }
6749
6750    pub fn staged_formula_count(&self) -> usize {
6751        self.staged_formulas.values().map(StagedSheet::len).sum()
6752    }
6753
6754    /// Stage a formula text instead of inserting into the graph (used when deferring is enabled).
6755    ///
6756    /// # Panics
6757    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6758    pub fn stage_formula_text(&mut self, sheet: &str, row: u32, col: u32, text: String) {
6759        self.assert_not_poisoned();
6760        self.staged_formulas
6761            .entry(sheet.to_string())
6762            .or_default()
6763            .stage(row, col, text);
6764        self.staged_formula_index.stage(sheet, row, col);
6765        if let Some(sheet) = self.graph.sheet_id(sheet) {
6766            self.invalidate_pending_spills(StructuralScope::Cell {
6767                sheet,
6768                row: row.saturating_sub(1),
6769                col: col.saturating_sub(1),
6770            });
6771        }
6772    }
6773
6774    fn index_removed_staged_sheet(&mut self, sheet: &str, staged: &StagedSheet) {
6775        for (row, col, _) in &staged.entries {
6776            self.staged_formula_index.remove(sheet, *row, *col);
6777        }
6778        if staged.deferred_package.is_some() {
6779            self.staged_formula_index.set_package(sheet, None);
6780        }
6781    }
6782
6783    fn restore_staged_sheet(&mut self, sheet: String, staged: StagedSheet) {
6784        self.staged_formulas.insert(sheet, staged);
6785    }
6786
6787    #[doc(hidden)]
6788    ///
6789    /// # Panics
6790    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6791    pub fn source_formula_ingress(&mut self) -> SourceFormulaIngress<'_, R> {
6792        self.assert_not_poisoned();
6793        SourceFormulaIngress { engine: self }
6794    }
6795
6796    #[doc(hidden)]
6797    /// Test-only fault-injection seam. Not part of the supported API; it exists so
6798    /// integration tests in sibling crates can fail a commit at an exact point.
6799    #[doc(hidden)]
6800    #[cfg(any(test, feature = "test-support"))]
6801    ///
6802    /// # Panics
6803    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6804    pub fn set_before_prepared_span_commit_hook(
6805        &mut self,
6806        hook: impl FnOnce() + Send + Sync + 'static,
6807    ) {
6808        self.assert_not_poisoned();
6809        self.before_prepared_span_commit_hook = Some(Box::new(hook));
6810    }
6811
6812    #[doc(hidden)]
6813    /// Test-only fault-injection seam, matching `set_after_eager_proposal_commit_hook`.
6814    #[cfg(test)]
6815    pub(crate) fn set_before_target_preparation_commit_hook(
6816        &mut self,
6817        hook: impl FnOnce() + Send + Sync + 'static,
6818    ) {
6819        self.before_target_preparation_commit_hook = Some(Box::new(hook));
6820    }
6821
6822    fn stage_deferred_formula_package(&mut self, package: crate::engine::DeferredFormulaPackage) {
6823        if let Ok(replay) = package.replay.lock()
6824            && let Some(footprint) = replay.selection_cache_footprint()
6825            && !self
6826                .source_cache_footprints
6827                .iter()
6828                .any(|known| known.ptr_eq(&footprint))
6829        {
6830            self.source_cache_footprints.push(footprint);
6831        }
6832        let sheet = package.sheet_name.clone();
6833        let staged = self.staged_formulas.entry(sheet.clone()).or_default();
6834        debug_assert!(staged.deferred_package.is_none());
6835        staged.deferred_package = Some(package);
6836        staged.reconcile_attached_deferred_package();
6837        self.staged_formula_index
6838            .set_package(&sheet, staged.deferred_package.as_ref());
6839        if let Some(sheet_id) = self.graph.sheet_id(&sheet) {
6840            let package = self
6841                .staged_formulas
6842                .get(&sheet)
6843                .and_then(|staged| staged.deferred_package.as_ref());
6844            for &(vertex, anchor, region) in &self.blocked_pending_spills {
6845                if anchor.sheet_id == sheet_id
6846                    && self.graph.vertex_exists(vertex)
6847                    && self.graph.get_cell_ref(vertex) == Some(anchor)
6848                    && self.staged_formula_index.package_occupies_spill(
6849                        &sheet,
6850                        (anchor.coord.row() + 1, anchor.coord.col() + 1),
6851                        (
6852                            region.rows.query_bounds().1 + 1,
6853                            region.cols.query_bounds().1 + 1,
6854                        ),
6855                        |point| package.is_some_and(|package| package.suppressed.contains(&point)),
6856                    )
6857                {
6858                    self.graph.mark_vertex_dirty(vertex);
6859                }
6860            }
6861        }
6862    }
6863
6864    ///
6865    /// # Panics
6866    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6867    pub fn clear_staged_formula_text(&mut self, sheet: &str, row: u32, col: u32) -> Option<String> {
6868        self.assert_not_poisoned();
6869        let mut removed = None;
6870        let mut remove_sheet = false;
6871        let mut had_package = false;
6872        if let Some(entries) = self.staged_formulas.get_mut(sheet) {
6873            had_package = entries.deferred_package.is_some();
6874            removed = entries.remove(row, col);
6875            remove_sheet = entries.is_empty();
6876        }
6877        let ordinary_removed = self.staged_formula_index.remove(sheet, row, col);
6878        if !ordinary_removed && had_package {
6879            self.staged_formula_index.touch_package(sheet);
6880        }
6881        if remove_sheet {
6882            self.staged_formulas.remove(sheet);
6883            self.staged_formula_index.set_package(sheet, None);
6884        }
6885        if (ordinary_removed || had_package)
6886            && let Some(sheet) = self.graph.sheet_id(sheet)
6887        {
6888            self.invalidate_pending_spills(StructuralScope::Cell {
6889                sheet,
6890                row: row.saturating_sub(1),
6891                col: col.saturating_sub(1),
6892            });
6893        }
6894        removed
6895    }
6896
6897    ///
6898    /// # Panics
6899    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6900    pub fn clear_staged_formulas_for_sheet(&mut self, sheet: &str) {
6901        self.assert_not_poisoned();
6902        if self.staged_formulas.remove(sheet).is_some() {
6903            self.staged_formula_index.clear_sheet(sheet);
6904            if let Some(sheet_id) = self.graph.sheet_id(sheet) {
6905                self.invalidate_pending_spills(StructuralScope::Sheet(sheet_id));
6906            }
6907        }
6908    }
6909
6910    ///
6911    /// # Panics
6912    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6913    pub fn rename_staged_formula_sheet(&mut self, old: &str, new: &str) {
6914        self.assert_not_poisoned();
6915        let Some(entries) = self.staged_formulas.remove(old) else {
6916            return;
6917        };
6918        self.staged_formula_index.clear_sheet(old);
6919        let (formulas, mut package) = entries.into_parts();
6920        for (row, col, text) in formulas {
6921            self.stage_formula_text(new, row, col, text);
6922        }
6923        if let Some(package) = package.as_mut() {
6924            package.sheet_name = new.to_string();
6925        }
6926        if let Some(package) = package {
6927            self.stage_deferred_formula_package(package);
6928        }
6929    }
6930
6931    /// Get a staged formula text for a given cell if present (cloned).
6932    pub fn get_staged_formula_text(&self, sheet: &str, row: u32, col: u32) -> Option<String> {
6933        self.staged_formulas
6934            .get(sheet)
6935            .and_then(|v| v.get(row, col))
6936    }
6937
6938    pub fn formula_parse_diagnostics(&self) -> &[FormulaParseDiagnostic] {
6939        &self.formula_parse_diagnostics
6940    }
6941
6942    ///
6943    /// # Panics
6944    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6945    pub fn take_formula_parse_diagnostics(&mut self) -> Vec<FormulaParseDiagnostic> {
6946        self.assert_not_poisoned();
6947        std::mem::take(&mut self.formula_parse_diagnostics)
6948    }
6949
6950    ///
6951    /// # Panics
6952    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
6953    pub fn clear_formula_parse_diagnostics(&mut self) {
6954        self.assert_not_poisoned();
6955        self.formula_parse_diagnostics.clear();
6956    }
6957
6958    pub fn last_formula_ingest_report(&self) -> Option<&FormulaIngestReport> {
6959        self.last_formula_ingest_report.as_ref()
6960    }
6961
6962    pub fn formula_ingest_report_total(&self) -> &FormulaIngestReport {
6963        &self.formula_ingest_report_total
6964    }
6965
6966    #[cfg(test)]
6967    pub(crate) fn set_before_target_planning_snapshot_hook_for_test(
6968        &mut self,
6969        hook: impl FnOnce() + Send + Sync + 'static,
6970    ) {
6971        self.before_target_planning_snapshot_hook = Some(Box::new(hook));
6972    }
6973
6974    #[cfg(test)]
6975    pub(crate) fn inject_target_semantic_stale_once_for_test(&mut self) {
6976        self.inject_target_semantic_stale_once_for_test = true;
6977    }
6978
6979    #[cfg(test)]
6980    pub(crate) fn force_virtual_dep_changes_for_test(&mut self, rounds: usize) {
6981        self.force_virtual_dep_changes_remaining_for_test = rounds;
6982    }
6983
6984    #[cfg(test)]
6985    pub(crate) fn fail_evaluation_commit_preflight_once_for_test(&mut self) {
6986        self.fail_evaluation_commit_preflight_once_for_test = true;
6987    }
6988
6989    #[cfg(test)]
6990    pub(crate) fn set_target_preparation_fault_for_test(
6991        &mut self,
6992        fault: crate::engine::target_preparation::TargetPreparationFault,
6993    ) {
6994        self.target_preparation_fault_for_test = Some(fault);
6995    }
6996
6997    #[cfg(test)]
6998    pub(crate) fn staged_formula_index_revision_for_test(&self) -> u64 {
6999        self.staged_formula_index.revision()
7000    }
7001
7002    #[cfg(test)]
7003    pub(crate) fn deferred_package_for_test(
7004        &self,
7005        sheet: &str,
7006    ) -> &crate::engine::DeferredFormulaPackage {
7007        self.staged_formulas
7008            .get(sheet)
7009            .unwrap()
7010            .deferred_package
7011            .as_ref()
7012            .unwrap()
7013    }
7014
7015    #[cfg(test)]
7016    pub(crate) fn staged_formula_index_is_consistent_for_test(&self) -> bool {
7017        let ordinary_storage = self
7018            .staged_formulas
7019            .values()
7020            .map(|sheet| sheet.entries.len())
7021            .sum::<usize>();
7022        let package_storage = self
7023            .staged_formulas
7024            .values()
7025            .filter(|sheet| sheet.deferred_package.is_some())
7026            .count();
7027        ordinary_storage == self.staged_formula_index.ordinary_count()
7028            && package_storage == self.staged_formula_index.package_count()
7029            && self.staged_formulas.iter().all(|(name, sheet)| {
7030                sheet.entries.iter().all(|(row, col, _)| {
7031                    let leases = self
7032                        .staged_formula_index
7033                        .leases_in_region(name, *row, *col, *row, *col);
7034                    leases.len() == 1 && leases[0].row == *row && leases[0].col == *col
7035                })
7036            })
7037    }
7038
7039    #[cfg(test)]
7040    pub(crate) fn evaluation_request_begin_count_for_test(&self) -> u64 {
7041        self.evaluation_request_begin_count_for_test
7042    }
7043
7044    #[cfg(test)]
7045    pub(crate) fn set_before_legacy_fallback_final_provider_sample_hook(
7046        &mut self,
7047        hook: impl FnOnce() + Send + Sync + 'static,
7048    ) {
7049        self.before_legacy_fallback_final_provider_sample_hook = Some(Box::new(hook));
7050    }
7051
7052    #[cfg(test)]
7053    pub(crate) fn set_after_eager_proposal_commit_hook(
7054        &mut self,
7055        hook: impl FnOnce() + Send + Sync + 'static,
7056    ) {
7057        self.after_eager_proposal_commit_hook = Some(Box::new(hook));
7058    }
7059
7060    #[cfg(test)]
7061    pub(crate) fn topology_epoch_for_test(&self) -> u64 {
7062        self.topology_epoch
7063    }
7064
7065    #[cfg(test)]
7066    pub(crate) fn graph_topology_revision_for_test(&self) -> u64 {
7067        self.graph.topology_revision()
7068    }
7069
7070    fn record_formula_ingest_report(&mut self, report: FormulaIngestReport) {
7071        self.formula_ingest_report_total.mode = report.mode;
7072        self.formula_ingest_report_total.accumulate(&report);
7073        self.last_formula_ingest_report = Some(report);
7074    }
7075
7076    fn collect_planning_function_requests(
7077        ast: &ASTNode,
7078        requests: &mut Vec<(String, String, usize)>,
7079    ) {
7080        match &ast.node_type {
7081            ASTNodeType::Function { name, args } => {
7082                requests.push((String::new(), name.clone(), args.len()));
7083                for arg in args {
7084                    Self::collect_planning_function_requests(arg, requests);
7085                }
7086            }
7087            ASTNodeType::BinaryOp { left, right, .. } => {
7088                Self::collect_planning_function_requests(left, requests);
7089                Self::collect_planning_function_requests(right, requests);
7090            }
7091            ASTNodeType::UnaryOp { expr, .. } => {
7092                Self::collect_planning_function_requests(expr, requests);
7093            }
7094            ASTNodeType::Call { callee, args } => {
7095                Self::collect_planning_function_requests(callee, requests);
7096                for arg in args {
7097                    Self::collect_planning_function_requests(arg, requests);
7098                }
7099            }
7100            ASTNodeType::Array(rows) => {
7101                for cell in rows.iter().flatten() {
7102                    Self::collect_planning_function_requests(cell, requests);
7103                }
7104            }
7105            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => {}
7106        }
7107    }
7108
7109    fn prepared_function_semantics_changed(
7110        &self,
7111        preparation: &crate::engine::FormulaCompressedPreparation,
7112        guard: &crate::function_registry::SemanticEpochReadGuard,
7113    ) -> bool {
7114        if preparation.function_semantic_epoch == guard.epoch() {
7115            return false;
7116        }
7117
7118        guard
7119            .semantic_changes_affect_requests_since(preparation.function_semantic_epoch, Vec::new())
7120    }
7121
7122    pub(crate) fn prepare_source_formula_families(
7123        &mut self,
7124        sheet_name: &str,
7125        families: &[crate::engine::SourceFormulaFamily],
7126    ) -> crate::engine::FormulaCompressedPreparation {
7127        crate::engine::FormulaCompressedPreparation {
7128            engine_token: Arc::clone(&self.source_formula_token),
7129            function_semantic_epoch: crate::function_registry::semantic_epoch(),
7130            function_provider_revision: None,
7131            function_semantics_used: false,
7132            sheet_name: Arc::from(sheet_name),
7133            rejected: BTreeMap::new(),
7134            eager_replay: Vec::new(),
7135            preparation_spool_replays: 0,
7136            clean_rejected_anchor_counts: [0; 3],
7137            fragmented_rejected_anchor_counts: [0; 3],
7138            exact_replay: None,
7139            replay_disposition: crate::engine::FormulaReplayDisposition::default(),
7140        }
7141    }
7142
7143    fn prepare_source_formula_proposals(
7144        &mut self,
7145        sheet_name: &str,
7146        families: &[crate::engine::SourceFormulaFamily],
7147        authority_partitions: &[crate::engine::PartitionedSourceFormulaFamily],
7148        replay_partitions: &[crate::engine::PartitionedSourceFormulaFamily],
7149        formula_record_count: u64,
7150        replay: Arc<std::sync::Mutex<Box<dyn crate::engine::DeferredFormulaReplay>>>,
7151        suppressed: &BTreeSet<(u32, u32)>,
7152        consumed: &FxHashSet<(crate::engine::SourceFamilyId, crate::engine::SourceCoord)>,
7153        consumed_engine: Option<&Arc<()>>,
7154    ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
7155        let mut preparation = self.prepare_source_formula_families(sheet_name, families);
7156        preparation.exact_replay = Some(Arc::clone(&replay));
7157        preparation
7158            .replay_disposition
7159            .register_consumed_members(consumed_engine, consumed.iter().copied());
7160        if !preparation
7161            .replay_disposition
7162            .consumed_engine_matches(&self.source_formula_token)
7163        {
7164            return Err(ExcelError::new(ExcelErrorKind::Value)
7165                .with_message("ResidualConsumedEngineMismatch"));
7166        }
7167        preparation
7168            .replay_disposition
7169            .extend_suppressed_excel_coords(suppressed.iter().copied());
7170        Ok(preparation)
7171    }
7172
7173    fn formula_batch_from_exact_replay(
7174        &mut self,
7175        sheet_name: &str,
7176        replayed: impl IntoIterator<Item = crate::engine::DeferredReplayFormula>,
7177    ) -> Result<FormulaIngestBatch, ExcelError> {
7178        let mut cache = rustc_hash::FxHashMap::default();
7179        let mut formulas = Vec::new();
7180        for record in replayed {
7181            let key = if record.text.starts_with('=') {
7182                record.text
7183            } else {
7184                format!("={}", record.text)
7185            };
7186            let ast_id = if let Some(cached) = cache.get(&key) {
7187                *cached
7188            } else {
7189                let parsed = match formualizer_parse::parser::parse(&key) {
7190                    Ok(parsed) => parsed,
7191                    Err(error) => {
7192                        let Some(parsed) = self.handle_formula_parse_error(
7193                            sheet_name,
7194                            record.row,
7195                            record.col,
7196                            &key,
7197                            error.to_string(),
7198                        )?
7199                        else {
7200                            continue;
7201                        };
7202                        parsed
7203                    }
7204                };
7205                let ast_id = self.intern_formula_ast(&parsed);
7206                cache.insert(key.clone(), ast_id);
7207                ast_id
7208            };
7209            formulas.push(
7210                FormulaIngestRecord::new(record.row, record.col, ast_id, Some(Arc::from(key)))
7211                    .with_source_proof(record.source_order, record.family, record.partition_owner),
7212            );
7213        }
7214        Ok(FormulaIngestBatch::new(sheet_name.to_string(), formulas))
7215    }
7216
7217    fn prepare_target_combined_legacy_graph(
7218        &self,
7219        packages: &[PreparedTargetSourcePackage],
7220        ordinary: &[PreparedOrdinaryStagedFormula],
7221    ) -> Result<(PreparedLegacyGraphPlan, usize), ExcelError> {
7222        let mut planned_by_coord = BTreeMap::new();
7223        for package in packages {
7224            for (row, col, ast_id, plan) in &package.legacy {
7225                planned_by_coord.insert((package.sheet_id, *row, *col), (*ast_id, plan.clone()));
7226            }
7227        }
7228        for formula in ordinary {
7229            if let Some((ast_id, plan)) = formula.ast_id.zip(formula.plan.clone()) {
7230                planned_by_coord.insert(
7231                    (formula.sheet_id, formula.lease.row, formula.lease.col),
7232                    (ast_id, plan),
7233                );
7234            }
7235        }
7236        let planned = planned_by_coord
7237            .into_iter()
7238            .map(|((sheet_id, row, col), (ast_id, plan))| (sheet_id, row, col, ast_id, plan))
7239            .collect::<Vec<_>>();
7240        let formula_count = planned.len();
7241        let graph = self
7242            .graph
7243            .prepare_legacy_graph_plan_multi_sheet(planned)
7244            .map_err(|error| {
7245                ExcelError::new(ExcelErrorKind::Value)
7246                    .with_message(format!("target graph preparation failed: {error}"))
7247            })?;
7248        Ok((graph, formula_count))
7249    }
7250
7251    fn replay_target_coordinates(
7252        &mut self,
7253        replay: &Arc<std::sync::Mutex<Box<dyn crate::engine::DeferredFormulaReplay>>>,
7254        coordinates: &[(u32, u32)],
7255        deadline: Option<std::time::Instant>,
7256        scratch: &mut u64,
7257    ) -> Result<Option<Vec<crate::engine::DeferredReplayFormula>>, ExcelError> {
7258        if coordinates.is_empty() {
7259            return Ok(Some(Vec::new()));
7260        }
7261        let mut guard = replay.lock().map_err(|_| {
7262            ExcelError::new(ExcelErrorKind::Value)
7263                .with_message("deferred formula spool lock poisoned")
7264        })?;
7265        let retained = guard.selection_cache_footprint().is_some();
7266        let records = guard.replay_selected_exact(coordinates, &mut |work, bytes| {
7267            self.target_preparation_checkpoint(deadline, work)?;
7268            if retained && let Some(ledger) = self.active_resource_ledger.as_mut() {
7269                ledger
7270                    .reserve_retained(bytes)
7271                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
7272                self.source_cache_accounted = self.source_cache_accounted.saturating_add(bytes);
7273            }
7274            self.reserve_graph_source_scratch(bytes)?;
7275            *scratch = scratch.saturating_add(bytes);
7276            Ok(())
7277        });
7278        drop(guard);
7279        let reconciled = self.reconcile_source_cache_footprints();
7280        let records = records?;
7281        reconciled?;
7282        Ok(records)
7283    }
7284
7285    fn prepare_target_exact_source_selection(
7286        &mut self,
7287        sheet: &str,
7288        lease: StagedPackageLease,
7289        coordinates: Vec<(u32, u32)>,
7290        previous: &BTreeSet<(u32, u32)>,
7291        allow_partial_shared: bool,
7292        deadline: Option<std::time::Instant>,
7293        scratch: &mut u64,
7294    ) -> Result<Option<PreparedTargetSourcePackage>, ExcelError> {
7295        let package = self
7296            .staged_formulas
7297            .get(sheet)
7298            .and_then(|s| s.deferred_package.as_ref())
7299            .unwrap();
7300        if !package.source_geometry_complete
7301            || ((!package.families.is_empty() || !package.partitioned_families.is_empty())
7302                && !package.coordinates_cover_families)
7303            || package.reconciliation_replay.is_some()
7304        {
7305            return Ok(None);
7306        }
7307        let selected_points: BTreeSet<_> = coordinates
7308            .into_iter()
7309            .filter(|point| !package.suppressed.contains(point))
7310            .collect();
7311        let mut prepared = PreparedTargetSourcePackage::empty_selection(
7312            sheet,
7313            self.graph.sheet_id(sheet).unwrap(),
7314            lease,
7315        );
7316        if selected_points.is_empty() {
7317            return Ok(Some(prepared));
7318        }
7319        let mut routing = crate::engine::FormulaReplayDisposition::default();
7320        for partition in &package.partitioned_families {
7321            routing
7322                .register_partition(partition, false)
7323                .map_err(|message| ExcelError::new(ExcelErrorKind::Value).with_message(message))?;
7324        }
7325        routing.extend_suppressed_excel_coords(package.suppressed.iter().copied());
7326        routing.register_consumed_members(
7327            package.consumed_engine.as_ref(),
7328            package.consumed_members.iter().copied(),
7329        );
7330        let replay = Arc::clone(&package.replay);
7331        let source_report = package.accounting_report();
7332        prepared.source_report = source_report;
7333        prepared.disposition = routing.clone();
7334
7335        let replay_points: BTreeSet<_> = selected_points
7336            .iter()
7337            .copied()
7338            .filter(|&(row, col)| !prepared.direct_contains(row, col))
7339            .collect();
7340        let coordinates: Vec<_> = replay_points.iter().copied().collect();
7341        let records = self.replay_target_coordinates(&replay, &coordinates, deadline, scratch)?;
7342        prepared.spool_replays = u64::from(!coordinates.is_empty());
7343        let Some(mut replay_records) = records else {
7344            return Ok(None);
7345        };
7346        for record in &mut replay_records {
7347            if record.family.is_none() {
7348                record.partition_owner = routing
7349                    .ordinary_disposition(crate::engine::SourceCoord {
7350                        row: record.row.saturating_sub(1),
7351                        col: record.col.saturating_sub(1),
7352                    })
7353                    .1;
7354            }
7355        }
7356        replay_records.sort_by_key(|record| record.source_order);
7357        if replay_records
7358            .iter()
7359            .any(|record| !selected_points.contains(&(record.row, record.col)))
7360            || replay_records
7361                .windows(2)
7362                .any(|records| records[0].source_order == records[1].source_order)
7363        {
7364            return Err(ExcelError::new(ExcelErrorKind::Value)
7365                .with_message("invalid indexed exact source selection"));
7366        }
7367        let represented: BTreeSet<_> = replay_records.iter().map(|r| (r.row, r.col)).collect();
7368        if represented != replay_points {
7369            return Err(ExcelError::new(ExcelErrorKind::Value)
7370                .with_message("incomplete indexed exact source selection"));
7371        }
7372        // The last source record must agree with compressed ownership evidence.
7373        // Earlier overridden records retain ordering but demand no dependencies.
7374        let source = self
7375            .staged_formulas
7376            .get(sheet)
7377            .unwrap()
7378            .deferred_package
7379            .as_ref()
7380            .unwrap();
7381        let contains = |rect: crate::engine::SourceRect, coord: crate::engine::SourceCoord| {
7382            coord.row >= rect.start.row
7383                && coord.row <= rect.end.row
7384                && coord.col >= rect.start.col
7385                && coord.col <= rect.end.col
7386        };
7387        let mut checked = BTreeSet::new();
7388        for record in replay_records.iter().rev() {
7389            if !checked.insert((record.row, record.col)) {
7390                continue;
7391            }
7392            let coord = crate::engine::SourceCoord {
7393                row: record.row - 1,
7394                col: record.col - 1,
7395            };
7396            let agrees = |owner, shared: bool| {
7397                record.family == shared.then_some(owner)
7398                    && record.partition_owner.or(record.family) == Some(owner)
7399            };
7400            let mut valid = true;
7401            for family in &source.families {
7402                let owns = match &family.members {
7403                    crate::engine::SourceFamilyMembers::CompleteDomain(domain) => {
7404                        contains(domain.rect(), coord)
7405                    }
7406                    crate::engine::SourceFamilyMembers::ExplicitMembers(members) => {
7407                        members.as_slice().binary_search(&coord).is_ok()
7408                    }
7409                };
7410                if owns && !agrees(family.source_id, true) {
7411                    valid = false;
7412                }
7413            }
7414            for family in &source.partitioned_families {
7415                if family
7416                    .fragments
7417                    .iter()
7418                    .any(|fragment| contains(fragment.rect(), coord))
7419                    && !agrees(family.source_id, true)
7420                {
7421                    valid = false;
7422                }
7423                if let Some(member) = family
7424                    .legacy_members
7425                    .as_slice()
7426                    .iter()
7427                    .find(|member| member.coord == coord)
7428                    && !agrees(
7429                        family.source_id,
7430                        member.kind == crate::engine::PartitionLegacyMemberKind::SharedFamilyMember,
7431                    )
7432                {
7433                    valid = false;
7434                }
7435            }
7436            if !valid {
7437                return Err(ExcelError::new(ExcelErrorKind::Value)
7438                    .with_message("indexed source ownership mismatch"));
7439            }
7440        }
7441        prepared.selected_points = Some(selected_points);
7442        prepared.replay_records = replay_records;
7443        Ok(Some(prepared))
7444    }
7445
7446    fn prepare_target_source_package(
7447        &mut self,
7448        sheet: &str,
7449        lease: StagedPackageLease,
7450        deadline: Option<std::time::Instant>,
7451    ) -> Result<PreparedTargetSourcePackage, ExcelError> {
7452        let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
7453            ExcelError::new(ExcelErrorKind::Ref)
7454                .with_message(format!("deferred source sheet not found: {sheet}"))
7455        })?;
7456        let (
7457            source_report,
7458            families,
7459            partitions,
7460            replay,
7461            invalidated,
7462            suppressed,
7463            consumed_members,
7464            consumed_engine,
7465            reconciliation_replay,
7466        ) = {
7467            let package = self
7468                .staged_formulas
7469                .get(sheet)
7470                .and_then(|staged| staged.deferred_package.as_ref())
7471                .ok_or_else(|| {
7472                    ExcelError::new(ExcelErrorKind::Value)
7473                        .with_message("staged deferred source package is unavailable")
7474                })?;
7475            if package.sheet_name != sheet {
7476                return Err(ExcelError::new(ExcelErrorKind::Value)
7477                    .with_message("deferred formula package sheet mismatch"));
7478            }
7479            (
7480                package.accounting_report(),
7481                package.families.clone(),
7482                package.partitioned_families.clone(),
7483                Arc::clone(&package.replay),
7484                package.invalidated.clone(),
7485                package.suppressed.clone(),
7486                package.consumed_members.clone(),
7487                package.consumed_engine.clone(),
7488                package.reconciliation_replay.clone(),
7489            )
7490        };
7491
7492        let mut replay_disposition = crate::engine::FormulaReplayDisposition::default();
7493        for partition in &partitions {
7494            replay_disposition
7495                .register_partition(partition, false)
7496                .map_err(|reason| ExcelError::new(ExcelErrorKind::Value).with_message(reason))?;
7497        }
7498        replay_disposition.extend_suppressed_excel_coords(suppressed.iter().copied());
7499        replay_disposition
7500            .register_consumed_members(consumed_engine.as_ref(), consumed_members.iter().copied());
7501        if !replay_disposition.consumed_engine_matches(&self.source_formula_token) {
7502            return Err(ExcelError::new(ExcelErrorKind::Value)
7503                .with_message("ResidualConsumedEngineMismatch"));
7504        }
7505        self.target_preparation_checkpoint(deadline, 1)?;
7506        let mut replay_records = if let Some(mut records) = reconciliation_replay {
7507            records.retain(|record| {
7508                let Some((row, col)) = record.row.checked_sub(1).zip(record.col.checked_sub(1))
7509                else {
7510                    return true;
7511                };
7512                let coord = crate::engine::SourceCoord { row, col };
7513                let disposition = record.family.map_or_else(
7514                    || replay_disposition.ordinary_disposition(coord).0,
7515                    |family| replay_disposition.shared_disposition(family, coord),
7516                );
7517                !matches!(
7518                    disposition,
7519                    crate::engine::FormulaReplayCoordinateDisposition::Direct
7520                        | crate::engine::FormulaReplayCoordinateDisposition::Suppressed
7521                )
7522            });
7523            records
7524        } else {
7525            replay
7526                .lock()
7527                .map_err(|_| {
7528                    ExcelError::new(ExcelErrorKind::Value)
7529                        .with_message("deferred formula spool lock poisoned")
7530                })?
7531                .replay_partitioned(&replay_disposition, &partitions)
7532                .map_err(|message| ExcelError::new(ExcelErrorKind::Value).with_message(message))?
7533        };
7534        replay_records.sort_by_key(|record| record.source_order);
7535        for chunk in replay_records.chunks(256) {
7536            self.target_preparation_checkpoint(deadline, chunk.len() as u64)?;
7537        }
7538        if replay_records
7539            .windows(2)
7540            .any(|records| records[0].source_order == records[1].source_order)
7541        {
7542            return Err(ExcelError::new(ExcelErrorKind::Value)
7543                .with_message("duplicate deferred source-order proof"));
7544        }
7545
7546        let disposition = replay_disposition;
7547        let direct_families = 0usize;
7548        let direct_cells = 0u64;
7549        let direct_fragments = 0u64;
7550        let direct_complete_families = 0u64;
7551        let direct_complete_cells = 0u64;
7552        let direct_partition_families = 0u64;
7553        let direct_partition_cells = 0u64;
7554        let anchor_parses = 0u64;
7555        let anchor_asts = 0u64;
7556        let anchor_analyses = 0u64;
7557
7558        Ok(PreparedTargetSourcePackage {
7559            sheet: sheet.to_string(),
7560            sheet_id,
7561            lease,
7562            selected_points: None,
7563            complete_selections: Default::default(),
7564            deferred_shared: false,
7565            direct_domains: Vec::new(),
7566            source_report,
7567            replay_records,
7568            spool_replays: 1,
7569            disposition,
7570            legacy: Vec::new(),
7571            direct_families,
7572            direct_cells,
7573            direct_fragments,
7574            direct_complete_families,
7575            direct_complete_cells,
7576            direct_partition_families,
7577            direct_partition_cells,
7578            anchor_parses,
7579            anchor_asts,
7580            anchor_analyses,
7581        })
7582    }
7583
7584    fn fallback_planning_snapshot(
7585        &self,
7586        batch: &FormulaIngestBatch,
7587    ) -> Result<crate::function_registry::RegistryPlanningSnapshot, ExcelError> {
7588        let mut requests = Vec::new();
7589        for formula in &batch.formulas {
7590            let ast = self
7591                .graph
7592                .data_store()
7593                .retrieve_ast(formula.ast_id, self.graph.sheet_reg())
7594                .ok_or_else(|| {
7595                    ExcelError::new(ExcelErrorKind::Value)
7596                        .with_message("ordered fallback AST is unavailable")
7597                })?;
7598            Self::collect_planning_function_requests(&ast, &mut requests);
7599        }
7600        requests.sort();
7601        requests.dedup();
7602        crate::function_registry::RegistryPlanningSnapshot::capture_for_requests(
7603            &self.resolver,
7604            requests,
7605        )
7606        .map_err(|error| ExcelError::new(ExcelErrorKind::Value).with_message(format!("{error:?}")))
7607    }
7608
7609    fn prepare_legacy_batch_fallback(
7610        &mut self,
7611        batch: FormulaIngestBatch,
7612        function_provider: &dyn crate::traits::FunctionProvider,
7613    ) -> Result<
7614        (
7615            crate::engine::graph::prepared_legacy_graph::PreparedLegacyGraphPlan,
7616            u64,
7617        ),
7618        ExcelError,
7619    > {
7620        let formula_count = batch.formulas.len() as u64;
7621        let sheet_id = self.graph.sheet_id_mut(&batch.sheet_name);
7622        let mut planned = Vec::with_capacity(batch.formulas.len());
7623        for record in batch.formulas {
7624            let placement = CellRef::new(
7625                sheet_id,
7626                Coord::from_excel(record.row, record.col, true, true),
7627            );
7628            let ingested = self
7629                .graph
7630                .ingest_pipeline(function_provider)
7631                .enable_function_semantics()
7632                .ingest_formula(
7633                    FormulaAstInput::RawArena(record.ast_id),
7634                    placement,
7635                    record.formula_text,
7636                )
7637                .map_err(|error| {
7638                    ExcelError::new(ExcelErrorKind::Value).with_message(format!("{error:?}"))
7639                })?;
7640            planned.push((record.row, record.col, ingested.ast_id, ingested.dep_plan));
7641        }
7642        let plan = self
7643            .graph
7644            .prepare_legacy_graph_plan(sheet_id, planned)
7645            .map_err(|error| {
7646                ExcelError::new(ExcelErrorKind::Value).with_message(error.to_string())
7647            })?;
7648        Ok((plan, formula_count))
7649    }
7650
7651    fn publish_compressed_partial_report(
7652        &mut self,
7653        report: &FormulaIngestReport,
7654        direct_report: &FormulaIngestReport,
7655    ) {
7656        if direct_report.source_family_promoted == 0
7657            && direct_report.graph_formula_cells_materialized == 0
7658        {
7659            return;
7660        }
7661        let mut published = report.clone();
7662        published.accumulate(direct_report);
7663        self.record_formula_ingest_report(published);
7664    }
7665
7666    fn finish_compressed_formula_sources(
7667        &mut self,
7668        batches: Vec<(
7669            FormulaIngestBatch,
7670            crate::engine::FormulaCompressedSourceReport,
7671            crate::engine::FormulaCompressedPreparation,
7672        )>,
7673    ) -> Result<FormulaIngestReport, ExcelError> {
7674        self.observe_function_semantic_epoch()?;
7675        if batches.iter().any(|(_, _, preparation)| {
7676            !Arc::ptr_eq(&preparation.engine_token, &self.source_formula_token)
7677        }) {
7678            return Err(ExcelError::new(ExcelErrorKind::Value)
7679                .with_message("compressed source preparation belongs to another engine"));
7680        }
7681        if batches.iter().any(|(fallback, _, preparation)| {
7682            preparation.sheet_name.as_ref() != fallback.sheet_name
7683        }) {
7684            return Err(ExcelError::new(ExcelErrorKind::Value)
7685                .with_message("compressed source preparation sheet mismatch"));
7686        }
7687        let initial_guard = crate::function_registry::semantic_epoch_read_guard();
7688        let initial_provider_revision = self.resolver.planning_semantic_revision();
7689        let mut fallback_batches = Vec::with_capacity(batches.len());
7690        let mut stale_fallback_batches = Vec::new();
7691        let mut pending_preparations = Vec::new();
7692        for (mut fallback, mut source, mut preparation) in batches {
7693            for formula in fallback.formulas.drain(..) {
7694                let source_order = formula.source_order.ok_or_else(|| {
7695                    ExcelError::new(ExcelErrorKind::Value).with_message(
7696                        "compressed source supplied formulas without source-order proof",
7697                    )
7698                })?;
7699                let text = formula.formula_text.ok_or_else(|| {
7700                    ExcelError::new(ExcelErrorKind::Value).with_message(
7701                        "ordered compressed fallback formula has no exact source text",
7702                    )
7703                })?;
7704                preparation
7705                    .eager_replay
7706                    .push(crate::engine::DeferredReplayFormula {
7707                        source_order,
7708                        row: formula.row,
7709                        col: formula.col,
7710                        text: text.to_string(),
7711                        family: formula.source_family,
7712                        partition_owner: formula.partition_owner,
7713                    });
7714            }
7715            preparation
7716                .eager_replay
7717                .sort_by_key(|record| record.source_order);
7718            source.source_spool_replays = source
7719                .source_spool_replays
7720                .saturating_add(preparation.preparation_spool_replays);
7721            let stale_reason = preparation
7722                .function_semantics_used
7723                .then(|| {
7724                    if preparation.function_provider_revision != initial_provider_revision {
7725                        Some("FunctionProviderRevisionChanged")
7726                    } else if self.prepared_function_semantics_changed(&preparation, &initial_guard)
7727                    {
7728                        Some("FunctionSemanticEpochChanged")
7729                    } else {
7730                        None
7731                    }
7732                })
7733                .flatten();
7734            let stale_semantics = stale_reason.is_some();
7735            for reason in preparation.rejected.values() {
7736                *source.fallback_reasons.entry(reason.clone()).or_default() += 1;
7737            }
7738            // No family is ever placed directly: every family replays.
7739            source.replay_families = source.families_seen;
7740            source.replay_cells = source.family_cells_seen;
7741            let compressed = crate::engine::FormulaCompressedSourceBatch::new(
7742                fallback.sheet_name.clone(),
7743                source,
7744            );
7745            if stale_semantics {
7746                stale_fallback_batches.push((fallback, compressed));
7747            } else {
7748                fallback_batches.push((fallback, compressed));
7749            }
7750            pending_preparations.push((preparation, stale_semantics));
7751        }
7752        drop(initial_guard);
7753
7754        // Build known fallback graphs first. Stale batches are forced through legacy ingest.
7755        let configured_mode = self.config.formula_plane_mode;
7756        self.config.formula_plane_mode = FormulaPlaneMode::Off;
7757        let stale_result =
7758            self.ingest_compressed_formula_source_batches_inner(stale_fallback_batches, false);
7759        self.config.formula_plane_mode = configured_mode;
7760        let mut report = stale_result?;
7761        report.mode = configured_mode;
7762
7763        self.config.formula_plane_mode = FormulaPlaneMode::Off;
7764        let fallback_result =
7765            self.ingest_compressed_formula_source_batches_inner(fallback_batches, false);
7766        self.config.formula_plane_mode = configured_mode;
7767        match fallback_result {
7768            Ok(fallback_report) => report.accumulate(&fallback_report),
7769            Err(error) => {
7770                self.record_formula_ingest_report(report);
7771                return Err(error);
7772            }
7773        }
7774
7775        let mut direct_report =
7776            FormulaIngestReport::with_mode(FormulaPlaneMode::AuthoritativeExperimental);
7777        for (preparation, _) in &pending_preparations {
7778            direct_report.source_anchor_parses = direct_report.source_anchor_parses.saturating_add(
7779                preparation.clean_rejected_anchor_counts[0]
7780                    .saturating_add(preparation.fragmented_rejected_anchor_counts[0]),
7781            );
7782            direct_report.source_anchor_asts = direct_report.source_anchor_asts.saturating_add(
7783                preparation.clean_rejected_anchor_counts[1]
7784                    .saturating_add(preparation.fragmented_rejected_anchor_counts[1]),
7785            );
7786            direct_report.source_anchor_analyses =
7787                direct_report.source_anchor_analyses.saturating_add(
7788                    preparation.clean_rejected_anchor_counts[2]
7789                        .saturating_add(preparation.fragmented_rejected_anchor_counts[2]),
7790                );
7791        }
7792        loop {
7793            #[cfg(any(test, feature = "test-support"))]
7794            if let Some(hook) = self.before_prepared_span_commit_hook.take() {
7795                hook();
7796            }
7797            let commit_guard = crate::function_registry::semantic_epoch_read_guard();
7798            let commit_provider_revision = self.resolver.planning_semantic_revision();
7799            let mut newly_stale = Vec::new();
7800            let mut current = Vec::new();
7801            for pending in pending_preparations.drain(..) {
7802                let stale_reason = pending
7803                    .0
7804                    .function_semantics_used
7805                    .then(|| {
7806                        if pending.0.function_provider_revision != commit_provider_revision {
7807                            Some("FunctionProviderRevisionChanged")
7808                        } else if self
7809                            .prepared_function_semantics_changed(&pending.0, &commit_guard)
7810                        {
7811                            Some("FunctionSemanticEpochChanged")
7812                        } else {
7813                            None
7814                        }
7815                    })
7816                    .flatten();
7817                if let Some(reason) = stale_reason {
7818                    newly_stale.push((pending, reason));
7819                } else {
7820                    current.push(pending);
7821                }
7822            }
7823            if !newly_stale.is_empty() {
7824                drop(commit_guard);
7825                for ((mut preparation, was_initially_stale), reason) in newly_stale {
7826                    preparation
7827                        .eager_replay
7828                        .sort_by_key(|record| record.source_order);
7829                    direct_report.source_spool_replays =
7830                        direct_report.source_spool_replays.saturating_add(1);
7831                    if !was_initially_stale {
7832                        direct_report
7833                            .fallback_reasons
7834                            .entry(reason.to_string())
7835                            .or_default();
7836                    }
7837                    preparation.function_semantics_used = false;
7838                    if !preparation.eager_replay.is_empty() {
7839                        current.push((preparation, false));
7840                    }
7841                }
7842                pending_preparations = current;
7843                continue;
7844            }
7845            drop(commit_guard);
7846
7847            // Replay fallback families in source order.
7848            enum SourceProposal {
7849                KnownFallback {
7850                    source_order: crate::engine::SourceFormulaOrder,
7851                    records: Vec<crate::engine::DeferredReplayFormula>,
7852                },
7853            }
7854
7855            impl SourceProposal {
7856                fn source_order(&self) -> crate::engine::SourceFormulaOrder {
7857                    match self {
7858                        Self::KnownFallback { source_order, .. } => *source_order,
7859                    }
7860                }
7861            }
7862
7863            for (mut preparation, _) in current {
7864                let mut proposals = Vec::with_capacity(preparation.eager_replay.len());
7865                let mut family_fallbacks: BTreeMap<_, Vec<_>> = BTreeMap::new();
7866                for record in preparation.eager_replay.drain(..) {
7867                    if let Some(owner) = record.partition_owner.or(record.family) {
7868                        family_fallbacks.entry(owner).or_default().push(record);
7869                    } else {
7870                        proposals.push(SourceProposal::KnownFallback {
7871                            source_order: record.source_order,
7872                            records: vec![record],
7873                        });
7874                    }
7875                }
7876                for (_, mut records) in family_fallbacks {
7877                    records.sort_by_key(|record| record.source_order);
7878                    if records
7879                        .windows(2)
7880                        .any(|window| window[0].source_order == window[1].source_order)
7881                    {
7882                        self.publish_compressed_partial_report(&report, &direct_report);
7883                        return Err(ExcelError::new(ExcelErrorKind::Value)
7884                            .with_message("duplicate exact-replay source-order proof"));
7885                    }
7886                    let Some(source_order) = records.first().map(|record| record.source_order)
7887                    else {
7888                        self.publish_compressed_partial_report(&report, &direct_report);
7889                        return Err(ExcelError::new(ExcelErrorKind::Value)
7890                            .with_message("empty exact-replay fallback family"));
7891                    };
7892                    proposals.push(SourceProposal::KnownFallback {
7893                        source_order,
7894                        records,
7895                    });
7896                }
7897                proposals.sort_by_key(SourceProposal::source_order);
7898                if proposals
7899                    .windows(2)
7900                    .any(|window| window[0].source_order() == window[1].source_order())
7901                {
7902                    self.publish_compressed_partial_report(&report, &direct_report);
7903                    return Err(ExcelError::new(ExcelErrorKind::Value)
7904                        .with_message("ambiguous compressed source-order proof"));
7905                }
7906
7907                for proposal in proposals {
7908                    match proposal {
7909                        SourceProposal::KnownFallback { records, .. } => {
7910                            let batch = match self.formula_batch_from_exact_replay(
7911                                preparation.sheet_name.as_ref(),
7912                                records,
7913                            ) {
7914                                Ok(batch) => batch,
7915                                Err(error) => {
7916                                    self.publish_compressed_partial_report(&report, &direct_report);
7917                                    return Err(error);
7918                                }
7919                            };
7920                            if batch.is_empty() {
7921                                continue;
7922                            }
7923                            let snapshot = match self.fallback_planning_snapshot(&batch) {
7924                                Ok(snapshot) => snapshot,
7925                                Err(error) => {
7926                                    self.publish_compressed_partial_report(&report, &direct_report);
7927                                    return Err(error);
7928                                }
7929                            };
7930                            let commit_guard =
7931                                crate::function_registry::semantic_epoch_read_guard();
7932                            let provider_revision_initial =
7933                                self.resolver.planning_semantic_revision();
7934                            if (commit_guard.epoch() != snapshot.epoch()
7935                                && snapshot.semantic_changes_affect_requests_since_guarded(
7936                                    &commit_guard,
7937                                    snapshot.epoch(),
7938                                ))
7939                                || snapshot.provider_revision().is_some_and(|revision| {
7940                                    Some(revision) != provider_revision_initial
7941                                })
7942                            {
7943                                drop(commit_guard);
7944                                self.publish_compressed_partial_report(&report, &direct_report);
7945                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
7946                                    "ordered fallback planning snapshot became stale",
7947                                ));
7948                            }
7949                            let (plan, formula_count) = match self
7950                                .prepare_legacy_batch_fallback(batch, &snapshot)
7951                            {
7952                                Ok(plan) => plan,
7953                                Err(error) => {
7954                                    drop(commit_guard);
7955                                    self.publish_compressed_partial_report(&report, &direct_report);
7956                                    return Err(error);
7957                                }
7958                            };
7959                            let provider_revision_after =
7960                                self.resolver.planning_semantic_revision();
7961                            if provider_revision_after != provider_revision_initial {
7962                                drop(commit_guard);
7963                                self.publish_compressed_partial_report(&report, &direct_report);
7964                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
7965                                    "function provider changed while preparing ordered fallback",
7966                                ));
7967                            }
7968                            let graph_vertices = plan.new_vertex_count();
7969                            let Some(graph_edges) = plan.planned_edge_count() else {
7970                                drop(commit_guard);
7971                                self.publish_compressed_partial_report(&report, &direct_report);
7972                                return Err(ExcelError::new(ExcelErrorKind::Value)
7973                                    .with_message("prepared ordered fallback size overflow"));
7974                            };
7975                            if let Err(error) = self.prepared_legacy_admission(&plan, formula_count)
7976                            {
7977                                drop(commit_guard);
7978                                self.publish_compressed_partial_report(&report, &direct_report);
7979                                return Err(error);
7980                            }
7981                            if let Err(error) =
7982                                self.graph.validate_prepared_legacy_graph_plan(&plan)
7983                            {
7984                                drop(commit_guard);
7985                                self.publish_compressed_partial_report(&report, &direct_report);
7986                                return Err(ExcelError::new(ExcelErrorKind::Value)
7987                                    .with_message(error.to_string()));
7988                            }
7989                            #[cfg(test)]
7990                            if let Some(hook) = self
7991                                .before_legacy_fallback_final_provider_sample_hook
7992                                .take()
7993                            {
7994                                hook();
7995                            }
7996                            let provider_revision_final =
7997                                self.resolver.planning_semantic_revision();
7998                            if provider_revision_final != provider_revision_initial {
7999                                drop(commit_guard);
8000                                self.publish_compressed_partial_report(&report, &direct_report);
8001                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
8002                                    "function provider changed after ordered fallback validation",
8003                                ));
8004                            }
8005                            let graph_formulas =
8006                                self.graph.apply_prevalidated_legacy_graph_plan(plan);
8007                            direct_report.formula_cells_seen = direct_report
8008                                .formula_cells_seen
8009                                .saturating_add(formula_count);
8010                            direct_report.graph_formula_cells_materialized = direct_report
8011                                .graph_formula_cells_materialized
8012                                .saturating_add(graph_formulas as u64);
8013                            direct_report.graph_vertices_created = direct_report
8014                                .graph_vertices_created
8015                                .saturating_add(graph_vertices as u64);
8016                            direct_report.graph_edges_created = direct_report
8017                                .graph_edges_created
8018                                .saturating_add(graph_edges as u64);
8019                        }
8020                    }
8021                    #[cfg(test)]
8022                    if let Some(hook) = self.after_eager_proposal_commit_hook.take() {
8023                        hook();
8024                    }
8025                }
8026            }
8027            break;
8028        }
8029        report.accumulate(&direct_report);
8030        self.record_formula_ingest_report(report.clone());
8031        Ok(report)
8032    }
8033    /// Ingest replayed per-cell formulas while preserving compressed source counters.
8034    pub(crate) fn ingest_compressed_formula_source_batches(
8035        &mut self,
8036        batches: Vec<(
8037            FormulaIngestBatch,
8038            crate::engine::FormulaCompressedSourceBatch,
8039        )>,
8040    ) -> Result<FormulaIngestReport, ExcelError> {
8041        self.ingest_compressed_formula_source_batches_inner(batches, true)
8042    }
8043
8044    fn ingest_compressed_formula_source_batches_inner(
8045        &mut self,
8046        batches: Vec<(
8047            FormulaIngestBatch,
8048            crate::engine::FormulaCompressedSourceBatch,
8049        )>,
8050        publish_report: bool,
8051    ) -> Result<FormulaIngestReport, ExcelError> {
8052        let mut source_counts = [0_u64; 11];
8053        let mut source_report = crate::engine::FormulaCompressedSourceReport::default();
8054        let mut formula_batches = Vec::with_capacity(batches.len());
8055        let mut compressed_families = Vec::new();
8056        let mut partitioned_families = Vec::new();
8057        for (batch, compressed_batch) in batches {
8058            let (sheet_name, compressed, families, partitions) = compressed_batch.into_parts();
8059            if sheet_name.as_ref() != batch.sheet_name {
8060                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
8061                    "compressed formula source sheet does not match its replay batch",
8062                ));
8063            }
8064            compressed_families.push((batch.sheet_name.clone(), families));
8065            partitioned_families.push((batch.sheet_name.clone(), partitions));
8066            source_counts[0] = source_counts[0].saturating_add(compressed.source_formula_events);
8067            source_counts[1] = source_counts[1].saturating_add(compressed.source_ordinary_events);
8068            source_counts[2] =
8069                source_counts[2].saturating_add(compressed.source_shared_anchor_events);
8070            source_counts[3] =
8071                source_counts[3].saturating_add(compressed.source_shared_descendant_events);
8072            source_counts[4] = source_counts[4].saturating_add(compressed.source_unknown_events);
8073            source_counts[5] =
8074                source_counts[5].saturating_add(compressed.source_formula_records_spooled);
8075            source_counts[6] =
8076                source_counts[6].saturating_add(compressed.source_spool_encoded_bytes);
8077            source_counts[7] = source_counts[7].max(compressed.source_spool_peak_memory_bytes);
8078            source_counts[8] =
8079                source_counts[8].saturating_add(compressed.source_spool_spilled_bytes);
8080            source_counts[9] = source_counts[9].saturating_add(compressed.source_spool_spill_files);
8081            source_counts[10] = source_counts[10].saturating_add(compressed.source_spool_replays);
8082            source_report.families_seen = source_report
8083                .families_seen
8084                .saturating_add(compressed.families_seen);
8085            source_report.family_cells_seen = source_report
8086                .family_cells_seen
8087                .saturating_add(compressed.family_cells_seen);
8088            source_report.source_clean_families = source_report
8089                .source_clean_families
8090                .saturating_add(compressed.source_clean_families);
8091            source_report.source_clean_cells = source_report
8092                .source_clean_cells
8093                .saturating_add(compressed.source_clean_cells);
8094            source_report.source_fragmentable_families = source_report
8095                .source_fragmentable_families
8096                .saturating_add(compressed.source_fragmentable_families);
8097            source_report.source_fragmentable_cells = source_report
8098                .source_fragmentable_cells
8099                .saturating_add(compressed.source_fragmentable_cells);
8100            source_report.source_fragment_count = source_report
8101                .source_fragment_count
8102                .saturating_add(compressed.source_fragment_count);
8103            source_report.source_isolated_fallback_cells = source_report
8104                .source_isolated_fallback_cells
8105                .saturating_add(compressed.source_isolated_fallback_cells);
8106            source_report.source_hole_exclusions = source_report
8107                .source_hole_exclusions
8108                .saturating_add(compressed.source_hole_exclusions);
8109            source_report.source_ordinary_exclusions = source_report
8110                .source_ordinary_exclusions
8111                .saturating_add(compressed.source_ordinary_exclusions);
8112            source_report.source_partition_failures = source_report
8113                .source_partition_failures
8114                .saturating_add(compressed.source_partition_failures);
8115            source_report.replay_families = source_report
8116                .replay_families
8117                .saturating_add(compressed.replay_families);
8118            source_report.replay_cells = source_report
8119                .replay_cells
8120                .saturating_add(compressed.replay_cells);
8121            source_report.forward_descendants = source_report
8122                .forward_descendants
8123                .saturating_add(compressed.forward_descendants);
8124            source_report.evidence_limit_fallbacks = source_report
8125                .evidence_limit_fallbacks
8126                .saturating_add(compressed.evidence_limit_fallbacks);
8127            source_report.evidence_peak_bytes = source_report
8128                .evidence_peak_bytes
8129                .max(compressed.evidence_peak_bytes);
8130            for (reason, count) in compressed.fallback_reasons {
8131                *source_report.fallback_reasons.entry(reason).or_default() += count;
8132            }
8133            formula_batches.push(batch);
8134        }
8135        self.ingest_formula_batches_inner(
8136            formula_batches,
8137            source_counts,
8138            Some(source_report),
8139            compressed_families,
8140            partitioned_families,
8141            publish_report,
8142        )
8143    }
8144
8145    pub fn ingest_formula_batches(
8146        &mut self,
8147        batches: Vec<FormulaIngestBatch>,
8148    ) -> Result<FormulaIngestReport, ExcelError> {
8149        self.ensure_not_poisoned()?;
8150        let has_formulas = batches.iter().any(|batch| !batch.formulas.is_empty());
8151        let report = self.ingest_formula_batches_inner(
8152            batches,
8153            [0; 11],
8154            None,
8155            Vec::new(),
8156            Vec::new(),
8157            true,
8158        )?;
8159        if has_formulas {
8160            self.mark_topology_edited();
8161        }
8162        Ok(report)
8163    }
8164
8165    fn ingest_formula_batches_unpublished(
8166        &mut self,
8167        batches: Vec<FormulaIngestBatch>,
8168    ) -> Result<FormulaIngestReport, ExcelError> {
8169        self.ingest_formula_batches_inner(batches, [0; 11], None, Vec::new(), Vec::new(), false)
8170    }
8171
8172    fn ingest_formula_batches_inner(
8173        &mut self,
8174        batches: Vec<FormulaIngestBatch>,
8175        source_counts: [u64; 11],
8176        source_report: Option<crate::engine::FormulaCompressedSourceReport>,
8177        compressed_families: Vec<(String, Vec<crate::engine::SourceFormulaFamily>)>,
8178        partitioned_families: Vec<(String, Vec<crate::engine::PartitionedSourceFormulaFamily>)>,
8179        publish_report: bool,
8180    ) -> Result<FormulaIngestReport, ExcelError> {
8181        self.observe_function_semantic_epoch()?;
8182        let formula_cells_seen = batches.iter().map(|batch| batch.len() as u64).sum();
8183        #[cfg(feature = "tracing")]
8184        let arena_nodes_before = self.graph.data_store().memory_usage().total_ast_nodes;
8185        #[cfg(feature = "tracing")]
8186        let route = if !partitioned_families.is_empty() {
8187            "partitioned"
8188        } else if !compressed_families.is_empty() {
8189            "compressed_source"
8190        } else if source_report.is_some() {
8191            "replay"
8192        } else {
8193            "ordinary"
8194        };
8195        let _ingest_span = crate::engine::trace::fz_span!(
8196            tracing::Level::INFO,
8197            "ingest",
8198            "ingest.batch",
8199            mode = ?FormulaPlaneMode::Off,
8200            route,
8201            formula_cells = formula_cells_seen
8202        );
8203        let mut report = FormulaIngestReport::with_mode(FormulaPlaneMode::Off);
8204        let materialize_batches = batches;
8205        report.formula_cells_seen = formula_cells_seen;
8206        report.source_formula_events = source_counts[0];
8207        report.source_ordinary_events = source_counts[1];
8208        report.source_shared_anchor_events = source_counts[2];
8209        report.source_shared_descendant_events = source_counts[3];
8210        report.source_unknown_events = source_counts[4];
8211        report.source_formula_records_spooled = source_counts[5];
8212        report.source_spool_encoded_bytes = source_counts[6];
8213        report.source_spool_peak_memory_bytes = source_counts[7];
8214        report.source_spool_spilled_bytes = source_counts[8];
8215        report.source_spool_spill_files = source_counts[9];
8216        report.source_spool_replays = source_counts[10];
8217        if let Some(source) = source_report {
8218            report.source_families_seen = source.families_seen;
8219            report.source_family_cells_seen = source.family_cells_seen;
8220            report.source_family_shadow_eligible = source.source_clean_families;
8221            report.source_family_shadow_eligible_cells = source.source_clean_cells;
8222            report.source_partitioned_families_seen = source.source_fragmentable_families;
8223            report.source_partition_holes = source.source_hole_exclusions;
8224            report.source_partition_ordinary_exceptions = source.source_ordinary_exclusions;
8225            report.source_partition_failures = source.source_partition_failures;
8226            report.source_partition_surviving_cells = source.source_fragmentable_cells;
8227            report.source_family_fallback = report
8228                .source_family_fallback
8229                .saturating_add(source.replay_families);
8230            report.source_family_fallback_cells = report
8231                .source_family_fallback_cells
8232                .saturating_add(source.replay_cells);
8233            report.source_forward_descendants = source.forward_descendants;
8234            report.source_evidence_limit_fallbacks = source.evidence_limit_fallbacks;
8235            report.source_evidence_peak_bytes = source.evidence_peak_bytes;
8236            for (reason, count) in source.fallback_reasons {
8237                let total = report.fallback_reasons.entry(reason).or_default();
8238                *total = total.saturating_add(count);
8239            }
8240        }
8241
8242        // A first load without graph admission plans in the builder, one
8243        // chunk at a time (a load error fails the load, so the builder's
8244        // partial application on a planning error is unobservable).
8245        if self.graph.first_load_assume_new()
8246            && !self.graph_admission_enabled()
8247            && !materialize_batches.iter().all(FormulaIngestBatch::is_empty)
8248        {
8249            let mut builder =
8250                crate::engine::ingest_builder::BulkIngestBuilder::new(&mut self.graph);
8251            for batch in materialize_batches {
8252                if batch.is_empty() {
8253                    continue;
8254                }
8255                let sheet_id = builder
8256                    .add_sheet_checked(&batch.sheet_name)
8257                    .ok_or_else(|| {
8258                        ExcelError::new(ExcelErrorKind::Ref)
8259                            .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
8260                    })?;
8261                builder.add_formula_refs(
8262                    sheet_id,
8263                    batch.formulas.into_iter().map(|record| {
8264                        (
8265                            record.row,
8266                            record.col,
8267                            crate::engine::graph::FormulaRef::of_ingested(
8268                                record.ast_id,
8269                                record.member_anchor,
8270                            ),
8271                        )
8272                    }),
8273                );
8274            }
8275            let summary = builder.finish_with_provider(&self.resolver)?;
8276            report.graph_formula_cells_materialized = summary.formulas as u64;
8277            report.graph_vertices_created = summary.vertices as u64;
8278            report.graph_edges_created = summary.edges as u64;
8279        } else if !materialize_batches.iter().all(FormulaIngestBatch::is_empty) {
8280            let mut prepared_by_sheet: BTreeMap<String, Vec<_>> = BTreeMap::new();
8281            for batch in materialize_batches {
8282                if batch.is_empty() {
8283                    continue;
8284                }
8285                let sheet_id = self.graph.sheet_id(&batch.sheet_name).ok_or_else(|| {
8286                    ExcelError::new(ExcelErrorKind::Ref)
8287                        .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
8288                })?;
8289                let mut pipeline = self.ingest_pipeline();
8290                // Plan each record and keep only what the graph needs (the
8291                // pipeline's per-formula facts are dropped at once).
8292                let prepared = prepared_by_sheet.entry(batch.sheet_name).or_default();
8293                prepared.reserve(batch.formulas.len());
8294                for record in batch.formulas {
8295                    let placement = CellRef::new(
8296                        sheet_id,
8297                        Coord::from_excel(record.row, record.col, true, true),
8298                    );
8299                    let input = match record.member_anchor {
8300                        Some(anchor) => FormulaAstInput::Member {
8301                            template: record.ast_id,
8302                            anchor,
8303                        },
8304                        None => FormulaAstInput::RawArena(record.ast_id),
8305                    };
8306                    let formula = pipeline.ingest_formula(input, placement, None)?;
8307                    prepared.push((
8308                        record.row,
8309                        record.col,
8310                        crate::engine::graph::FormulaRef::of_ingested(
8311                            formula.ast_id,
8312                            formula.member_anchor,
8313                        ),
8314                        formula.dep_plan,
8315                    ));
8316                }
8317            }
8318            let admission_preflighted = self.graph_admission_enabled();
8319            if admission_preflighted {
8320                let mut preview = Vec::new();
8321                for (sheet_name, formulas) in &prepared_by_sheet {
8322                    let sheet_id = self.graph.sheet_id(sheet_name).ok_or_else(|| {
8323                        ExcelError::new(ExcelErrorKind::Ref)
8324                            .with_message(format!("unknown ingest sheet: {sheet_name}"))
8325                    })?;
8326                    preview.extend(
8327                        formulas
8328                            .iter()
8329                            .map(|(row, col, _, plan)| (sheet_id, *row, *col, plan.clone())),
8330                    );
8331                }
8332                let admission = self.graph.preview_formula_mutations(&preview)?;
8333                self.preflight_graph_admission(admission)?;
8334            }
8335
8336            let mut builder = self.begin_bulk_ingest();
8337            if admission_preflighted {
8338                builder.mark_admission_preflighted();
8339            }
8340            for (sheet_name, formulas) in prepared_by_sheet {
8341                if formulas.is_empty() {
8342                    continue;
8343                }
8344                let sheet_id = builder.add_sheet(&sheet_name);
8345                builder.add_formula_plans(sheet_id, formulas);
8346            }
8347            let summary = builder.finish()?;
8348            report.graph_formula_cells_materialized = summary.formulas as u64;
8349            report.graph_vertices_created = summary.vertices as u64;
8350            report.graph_edges_created = summary.edges as u64;
8351        }
8352
8353        crate::engine::trace::fz_event!(
8354            tracing::Level::INFO,
8355            "ingest",
8356            "ingest.summary",
8357            candidate_cells = report.shadow_candidate_cells,
8358            accepted_span_cells = report.shadow_accepted_span_cells,
8359            fallback_cells = report.shadow_fallback_cells,
8360            spans_created = report.shadow_spans_created,
8361            templates_interned = report.shadow_templates_interned,
8362            graph_vertices_created = report.graph_vertices_created,
8363            graph_edges_created = report.graph_edges_created,
8364            arena_nodes_delta = self
8365                .graph
8366                .data_store()
8367                .memory_usage()
8368                .total_ast_nodes
8369                .saturating_sub(arena_nodes_before)
8370        );
8371        if publish_report {
8372            self.record_formula_ingest_report(report.clone());
8373        }
8374        Ok(report)
8375    }
8376
8377    fn dedup_formula_parse_diagnostics_since(&mut self, start: usize) {
8378        let mut unique = Vec::new();
8379        for diagnostic in self.formula_parse_diagnostics.drain(start..) {
8380            let duplicate = unique.iter().any(|prior: &FormulaParseDiagnostic| {
8381                prior.sheet == diagnostic.sheet
8382                    && prior.row == diagnostic.row
8383                    && prior.col == diagnostic.col
8384                    && prior.formula == diagnostic.formula
8385                    && prior.policy == diagnostic.policy
8386            });
8387            if !duplicate {
8388                unique.push(diagnostic);
8389            }
8390        }
8391        self.formula_parse_diagnostics.extend(unique);
8392    }
8393
8394    pub fn handle_formula_parse_error(
8395        &mut self,
8396        sheet: &str,
8397        row: u32,
8398        col: u32,
8399        formula: &str,
8400        message: String,
8401    ) -> Result<Option<ASTNode>, ExcelError> {
8402        self.ensure_not_poisoned()?;
8403        let policy = self.config.formula_parse_policy;
8404
8405        if policy == FormulaParsePolicy::Strict {
8406            let col_a1 = col_letters_from_1based(col).unwrap_or_else(|_| "?".to_string());
8407            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
8408                "Formula parse error at {sheet}!{col_a1}{row}: {message}"
8409            )));
8410        }
8411
8412        self.formula_parse_diagnostics.push(FormulaParseDiagnostic {
8413            sheet: sheet.to_string(),
8414            row,
8415            col,
8416            formula: formula.to_string(),
8417            message: message.clone(),
8418            policy,
8419        });
8420
8421        match policy {
8422            FormulaParsePolicy::Strict => unreachable!(),
8423            FormulaParsePolicy::KeepCachedValue => Ok(None),
8424            FormulaParsePolicy::AsText => Ok(Some(ASTNode::new(
8425                ASTNodeType::Literal(LiteralValue::Text(formula.to_string())),
8426                None,
8427            ))),
8428            FormulaParsePolicy::CoerceToError => {
8429                let err = ExcelError::new(ExcelErrorKind::Error)
8430                    .with_message(format!("Malformed formula: {message}"));
8431                Ok(Some(ASTNode::new(
8432                    ASTNodeType::Literal(LiteralValue::Error(err)),
8433                    None,
8434                )))
8435            }
8436        }
8437    }
8438
8439    #[cfg(test)]
8440    fn target_preparation_fault(
8441        &mut self,
8442        seam: crate::engine::target_preparation::TargetPreparationFault,
8443    ) -> Result<(), ExcelError> {
8444        if self.target_preparation_fault_for_test == Some(seam) {
8445            self.target_preparation_fault_for_test = None;
8446            Err(ExcelError::new(ExcelErrorKind::Value)
8447                .with_message(format!("injected target preparation fault: {seam:?}")))
8448        } else {
8449            Ok(())
8450        }
8451    }
8452
8453    fn preparation_stale(
8454        reason: formualizer_common::PreparationStaleReason,
8455        message: impl Into<String>,
8456    ) -> ExcelError {
8457        ExcelError::new(ExcelErrorKind::Value)
8458            .with_message(message)
8459            .with_extra(formualizer_common::ExcelErrorExtra::PreparationStale { reason })
8460    }
8461
8462    fn preparation_revision_stale_reason(
8463        assumptions: &crate::engine::PreparationRevision,
8464        current: &crate::engine::PreparationRevision,
8465        planning_requests: &BTreeSet<(String, String, usize)>,
8466        staged_leases_match: bool,
8467    ) -> Option<formualizer_common::PreparationStaleReason> {
8468        if assumptions.graph != current.graph {
8469            Some(formualizer_common::PreparationStaleReason::Graph)
8470        } else if assumptions.staged != current.staged || !staged_leases_match {
8471            Some(formualizer_common::PreparationStaleReason::Staged)
8472        } else if assumptions.symbols != current.symbols {
8473            Some(formualizer_common::PreparationStaleReason::Symbols)
8474        } else if assumptions.provider != current.provider {
8475            Some(formualizer_common::PreparationStaleReason::Provider)
8476        } else if assumptions.semantic != current.semantic
8477            && crate::function_registry::semantic_changes_affect_requests_since(
8478                assumptions.semantic,
8479                planning_requests.iter().cloned(),
8480            )
8481        {
8482            Some(formualizer_common::PreparationStaleReason::Semantic)
8483        } else {
8484            None
8485        }
8486    }
8487
8488    fn preparation_revisions(&self) -> crate::engine::PreparationRevision {
8489        crate::engine::PreparationRevision {
8490            graph: self.graph.topology_revision(),
8491            authority: 0,
8492            authority_indexes: 0,
8493            authority_indexed_plane: 0,
8494            staged: self.staged_formula_index.revision(),
8495            symbols: self.graph.symbol_revision(),
8496            semantic: crate::function_registry::semantic_epoch(),
8497            provider: self.resolver.planning_semantic_revision(),
8498        }
8499    }
8500
8501    fn planning_revision_snapshot(&self) -> PlanningRevisionSnapshot {
8502        let registry_guard = crate::function_registry::semantic_epoch_read_guard();
8503        let provider = self.resolver.planning_semantic_revision();
8504        let semantic = registry_guard.epoch();
8505        PlanningRevisionSnapshot {
8506            engine_topology_epoch: self.topology_epoch,
8507            graph_topology_revision: self.graph.topology_revision(),
8508            staged: self.staged_formula_index.revision(),
8509            symbols: self.graph.symbol_revision(),
8510            semantic,
8511            provider,
8512            deterministic_mode: self.config.deterministic_mode.clone(),
8513            budgets: self.evaluation_resource_budgets.clone(),
8514        }
8515    }
8516
8517    fn recalc_plan_key(&self) -> RecalcPlanKey {
8518        RecalcPlanKey {
8519            engine_token: Arc::clone(&self.recalc_plan_token),
8520            revisions: self.planning_revision_snapshot(),
8521        }
8522    }
8523
8524    fn plan_stale(reason: formualizer_common::PlanStaleReason) -> ExcelError {
8525        ExcelError::new(ExcelErrorKind::Value)
8526            .with_message(format!("recalculation plan is stale: {}", reason.as_str()))
8527            .with_extra(formualizer_common::ExcelErrorExtra::PlanStale { reason })
8528    }
8529
8530    fn validate_recalc_plan_key(&self, key: &RecalcPlanKey) -> Result<(), ExcelError> {
8531        use formualizer_common::PlanStaleReason;
8532
8533        if !Arc::ptr_eq(&key.engine_token, &self.recalc_plan_token) {
8534            return Err(Self::plan_stale(PlanStaleReason::Engine));
8535        }
8536
8537        let current = self.planning_revision_snapshot();
8538        let expected = &key.revisions;
8539        let stale = if expected.provider != current.provider {
8540            Some(PlanStaleReason::Provider)
8541        } else if expected.semantic != current.semantic {
8542            Some(PlanStaleReason::Semantic)
8543        } else if expected.budgets != current.budgets
8544            || expected.deterministic_mode != current.deterministic_mode
8545        {
8546            Some(PlanStaleReason::Budget)
8547        } else if expected.staged != current.staged {
8548            Some(PlanStaleReason::Staged)
8549        } else if expected.symbols != current.symbols {
8550            Some(PlanStaleReason::Symbols)
8551        } else if expected.graph_topology_revision != current.graph_topology_revision
8552            || expected.engine_topology_epoch != current.engine_topology_epoch
8553        {
8554            Some(PlanStaleReason::Graph)
8555        } else {
8556            None
8557        };
8558        stale.map_or(Ok(()), |reason| Err(Self::plan_stale(reason)))
8559    }
8560
8561    fn target_preparation_checkpoint(
8562        &mut self,
8563        deadline: Option<std::time::Instant>,
8564        work: u64,
8565    ) -> Result<(), ExcelError> {
8566        if self
8567            .active_cancel_flag
8568            .as_ref()
8569            .is_some_and(|cancel| cancel.is_cancelled())
8570        {
8571            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
8572                .with_message("target graph preparation cancelled"));
8573        }
8574        if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) {
8575            return Err(crate::engine::ResourceLedgerError::Exhausted(
8576                formualizer_common::ResourceExhaustionDetail {
8577                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
8578                    limit: 0,
8579                    observed: 1,
8580                    request_id: self
8581                        .active_evaluation_resource_request
8582                        .as_ref()
8583                        .map(|stats| stats.request_id),
8584                },
8585            )
8586            .into_excel_error());
8587        }
8588        self.charge_bounded_work(work)
8589    }
8590
8591    fn opaque_reason_in_ast(
8592        &self,
8593        ast: &ASTNode,
8594        provider: &crate::function_registry::RegistryPlanningSnapshot,
8595    ) -> Option<crate::engine::OpaqueReason> {
8596        match &ast.node_type {
8597            ASTNodeType::Function { name, args } => {
8598                let canonical = name.rsplit('.').next().unwrap_or(name).to_ascii_uppercase();
8599                if canonical == "INDIRECT" {
8600                    return Some(crate::engine::OpaqueReason::RuntimeTextReference);
8601                }
8602                let Some(caps) = provider.function_capabilities("", name) else {
8603                    return Some(crate::engine::OpaqueReason::UnknownFunction);
8604                };
8605                if caps.contains(FnCaps::DYNAMIC_DEPENDENCY)
8606                    || caps.contains(FnCaps::RETURNS_REFERENCE)
8607                {
8608                    return Some(crate::engine::OpaqueReason::DynamicReference);
8609                }
8610                args.iter()
8611                    .find_map(|arg| self.opaque_reason_in_ast(arg, provider))
8612            }
8613            ASTNodeType::Call { .. } => Some(crate::engine::OpaqueReason::UnknownCustomFunction),
8614            ASTNodeType::UnaryOp { expr, .. } => self.opaque_reason_in_ast(expr, provider),
8615            ASTNodeType::BinaryOp { left, right, .. } => self
8616                .opaque_reason_in_ast(left, provider)
8617                .or_else(|| self.opaque_reason_in_ast(right, provider)),
8618            ASTNodeType::Array(rows) => rows
8619                .iter()
8620                .flat_map(|row| row.iter())
8621                .find_map(|item| self.opaque_reason_in_ast(item, provider)),
8622            ASTNodeType::Reference {
8623                reference:
8624                    ReferenceType::Cell {
8625                        sheet: Some(sheet), ..
8626                    }
8627                    | ReferenceType::Range {
8628                        sheet: Some(sheet), ..
8629                    },
8630                ..
8631            } if self.graph.sheet_id(sheet).is_none() => {
8632                Some(crate::engine::OpaqueReason::UnresolvedCrossSheetBinding)
8633            }
8634            ASTNodeType::Reference {
8635                reference:
8636                    ReferenceType::External(_)
8637                    | ReferenceType::Cell3D { .. }
8638                    | ReferenceType::Range3D { .. },
8639                ..
8640            } => Some(crate::engine::OpaqueReason::UnresolvedCrossSheetBinding),
8641            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => None,
8642        }
8643    }
8644
8645    fn target_planning_snapshot(
8646        &mut self,
8647        ast: &ASTNode,
8648        planning_requests: &mut BTreeSet<(String, String, usize)>,
8649    ) -> Result<crate::function_registry::RegistryPlanningSnapshot, ExcelError> {
8650        #[cfg(test)]
8651        if std::mem::take(&mut self.inject_target_semantic_stale_once_for_test) {
8652            return Err(Self::preparation_stale(
8653                formualizer_common::PreparationStaleReason::Semantic,
8654                "injected target semantic preparation movement",
8655            ));
8656        }
8657        #[cfg(test)]
8658        if let Some(hook) = self.before_target_planning_snapshot_hook.take() {
8659            hook();
8660        }
8661        let mut requests = Vec::new();
8662        Self::collect_planning_function_requests(ast, &mut requests);
8663        requests.sort();
8664        requests.dedup();
8665        planning_requests.extend(requests.iter().cloned());
8666        crate::function_registry::RegistryPlanningSnapshot::capture_for_requests(
8667            &self.resolver,
8668            requests,
8669        )
8670        .map_err(|error| {
8671            ExcelError::new(ExcelErrorKind::Value)
8672                .with_message(format!("target planning snapshot unavailable: {error:?}"))
8673        })
8674    }
8675
8676    fn target_planning_snapshot_stale_reason(
8677        snapshot: &crate::function_registry::RegistryPlanningSnapshot,
8678        assumptions: &crate::engine::PreparationRevision,
8679    ) -> Option<formualizer_common::PreparationStaleReason> {
8680        if snapshot
8681            .provider_revision()
8682            .is_some_and(|revision| Some(revision) != assumptions.provider)
8683        {
8684            Some(formualizer_common::PreparationStaleReason::Provider)
8685        } else if snapshot.epoch() != assumptions.semantic
8686            && snapshot.semantic_changes_affect_requests_since(assumptions.semantic)
8687        {
8688            Some(formualizer_common::PreparationStaleReason::Semantic)
8689        } else {
8690            None
8691        }
8692    }
8693
8694    fn widen_target_preparation(
8695        policy: crate::engine::OpaquePreparePolicy,
8696        scope: &mut crate::engine::PrepareScope,
8697        reasons: &mut Vec<crate::engine::OpaqueReason>,
8698        reason: crate::engine::OpaqueReason,
8699    ) -> Result<bool, ExcelError> {
8700        if policy == crate::engine::OpaquePreparePolicy::Error {
8701            return Err(ExcelError::new(ExcelErrorKind::NImpl)
8702                .with_message(format!("opaque target preparation semantics: {reason:?}")));
8703        }
8704        if !reasons.contains(&reason) {
8705            reasons.push(reason);
8706        }
8707        if !matches!(scope, crate::engine::PrepareScope::Workbook) {
8708            *scope = crate::engine::PrepareScope::Workbook;
8709            Ok(true)
8710        } else {
8711            Ok(false)
8712        }
8713    }
8714
8715    fn widen_target_preparation_to_sheet(
8716        policy: crate::engine::OpaquePreparePolicy,
8717        scope: &mut crate::engine::PrepareScope,
8718        reasons: &mut Vec<crate::engine::OpaqueReason>,
8719        reason: crate::engine::OpaqueReason,
8720        sheet: &str,
8721    ) -> Result<bool, ExcelError> {
8722        if policy == crate::engine::OpaquePreparePolicy::Error {
8723            return Err(ExcelError::new(ExcelErrorKind::NImpl)
8724                .with_message(format!("opaque target preparation semantics: {reason:?}")));
8725        }
8726        if !reasons.contains(&reason) {
8727            reasons.push(reason);
8728        }
8729        match scope {
8730            crate::engine::PrepareScope::Exact => {
8731                *scope = crate::engine::PrepareScope::Sheets(vec![sheet.to_string()]);
8732                Ok(true)
8733            }
8734            crate::engine::PrepareScope::Sheets(sheets) => {
8735                if sheets.iter().any(|candidate| candidate == sheet) {
8736                    Ok(false)
8737                } else {
8738                    sheets.push(sheet.to_string());
8739                    sheets.sort();
8740                    Ok(true)
8741                }
8742            }
8743            crate::engine::PrepareScope::Workbook => Ok(false),
8744        }
8745    }
8746
8747    fn ast_has_proven_sheet_local_dynamic(
8748        ast: &ASTNode,
8749        provider: &crate::function_registry::RegistryPlanningSnapshot,
8750    ) -> bool {
8751        fn classify(
8752            ast: &ASTNode,
8753            provider: &crate::function_registry::RegistryPlanningSnapshot,
8754        ) -> (bool, bool) {
8755            match &ast.node_type {
8756                ASTNodeType::Function { name, args } => {
8757                    let Some(caps) = provider.function_capabilities("", name) else {
8758                        return (false, false);
8759                    };
8760                    let dynamic = caps.contains(FnCaps::DYNAMIC_DEPENDENCY)
8761                        || caps.contains(FnCaps::RETURNS_REFERENCE);
8762                    let canonical = name.rsplit('.').next().unwrap_or(name);
8763                    if dynamic
8764                        && !canonical.eq_ignore_ascii_case("OFFSET")
8765                        && !canonical.eq_ignore_ascii_case("INDEX")
8766                    {
8767                        return (false, true);
8768                    }
8769                    let mut has_dynamic = dynamic;
8770                    for arg in args {
8771                        let (safe, child_dynamic) = classify(arg, provider);
8772                        if !safe {
8773                            return (false, has_dynamic || child_dynamic);
8774                        }
8775                        has_dynamic |= child_dynamic;
8776                    }
8777                    (true, has_dynamic)
8778                }
8779                ASTNodeType::UnaryOp { expr, .. } => classify(expr, provider),
8780                ASTNodeType::BinaryOp { left, right, .. } => {
8781                    let (left_safe, left_dynamic) = classify(left, provider);
8782                    let (right_safe, right_dynamic) = classify(right, provider);
8783                    (left_safe && right_safe, left_dynamic || right_dynamic)
8784                }
8785                ASTNodeType::Array(rows) => {
8786                    let mut has_dynamic = false;
8787                    for item in rows.iter().flatten() {
8788                        let (safe, child_dynamic) = classify(item, provider);
8789                        if !safe {
8790                            return (false, has_dynamic || child_dynamic);
8791                        }
8792                        has_dynamic |= child_dynamic;
8793                    }
8794                    (true, has_dynamic)
8795                }
8796                ASTNodeType::Reference {
8797                    reference:
8798                        ReferenceType::Cell { sheet: None, .. }
8799                        | ReferenceType::Range { sheet: None, .. },
8800                    ..
8801                }
8802                | ASTNodeType::Literal(_)
8803                | ASTNodeType::Omitted => (true, false),
8804                ASTNodeType::Call { .. } | ASTNodeType::Reference { .. } => (false, false),
8805            }
8806        }
8807
8808        let (safe, dynamic) = classify(ast, provider);
8809        safe && dynamic
8810    }
8811
8812    fn table_selection_region(
8813        &self,
8814        entry: &crate::engine::graph::TableEntry,
8815        selection: &crate::engine::TableSelection,
8816    ) -> Result<PreparationRegion, ExcelError> {
8817        let mut start_row = entry.range.start.coord.row() + 1;
8818        let mut end_row = entry.range.end.coord.row() + 1;
8819        let mut start_col = entry.range.start.coord.col() + 1;
8820        let mut end_col = entry.range.end.coord.col() + 1;
8821        match selection {
8822            crate::engine::TableSelection::Whole => {}
8823            crate::engine::TableSelection::Headers => {
8824                if !entry.header_row {
8825                    return Err(ExcelError::new(ExcelErrorKind::Value)
8826                        .with_message(format!("table {} has no header row", entry.name)));
8827                }
8828                end_row = start_row;
8829            }
8830            crate::engine::TableSelection::Data => {
8831                if entry.header_row {
8832                    start_row = start_row.saturating_add(1);
8833                }
8834                if entry.totals_row {
8835                    end_row = end_row.saturating_sub(1);
8836                }
8837            }
8838            crate::engine::TableSelection::Totals => {
8839                if !entry.totals_row {
8840                    return Err(ExcelError::new(ExcelErrorKind::Value)
8841                        .with_message(format!("table {} has no totals row", entry.name)));
8842                }
8843                start_row = end_row;
8844            }
8845            crate::engine::TableSelection::Column(column) => {
8846                let index = entry.col_index(column).ok_or_else(|| {
8847                    ExcelError::new(ExcelErrorKind::Name)
8848                        .with_message(format!("unknown table column: {column}"))
8849                })?;
8850                start_col = start_col.saturating_add(index as u32);
8851                end_col = start_col;
8852            }
8853            crate::engine::TableSelection::Columns { start, end } => {
8854                let first = entry.col_index(start).ok_or_else(|| {
8855                    ExcelError::new(ExcelErrorKind::Name)
8856                        .with_message(format!("unknown table column: {start}"))
8857                })?;
8858                let last = entry.col_index(end).ok_or_else(|| {
8859                    ExcelError::new(ExcelErrorKind::Name)
8860                        .with_message(format!("unknown table column: {end}"))
8861                })?;
8862                if first > last {
8863                    return Err(ExcelError::new(ExcelErrorKind::Value)
8864                        .with_message("table column selection is reversed"));
8865                }
8866                start_col = start_col.saturating_add(first as u32);
8867                end_col = entry
8868                    .range
8869                    .start
8870                    .coord
8871                    .col()
8872                    .saturating_add(last as u32)
8873                    .saturating_add(1);
8874            }
8875        }
8876        if start_row > end_row && matches!(selection, crate::engine::TableSelection::Data) {
8877            start_row = entry.range.start.coord.row() + 1;
8878            end_row = start_row;
8879        }
8880        if start_row > end_row || start_col > end_col {
8881            return Err(
8882                ExcelError::new(ExcelErrorKind::Value).with_message("table selection is empty")
8883            );
8884        }
8885        Ok(PreparationRegion {
8886            sheet: self.graph.sheet_name(entry.sheet_id()).to_string(),
8887            sheet_id: entry.sheet_id(),
8888            start_row,
8889            start_col,
8890            end_row,
8891            end_col,
8892        })
8893    }
8894
8895    #[cfg(test)]
8896    pub(crate) fn reset_target_root_dedup_probes_for_test() {
8897        TARGET_ROOT_DEDUP_PROBES.with(|probes| probes.set(0));
8898    }
8899
8900    #[cfg(test)]
8901    pub(crate) fn target_root_dedup_probes_for_test() -> usize {
8902        TARGET_ROOT_DEDUP_PROBES.with(std::cell::Cell::get)
8903    }
8904
8905    pub(crate) fn resolve_target_producers(
8906        &mut self,
8907        targets: &[crate::engine::EvaluationTarget],
8908    ) -> Result<Vec<crate::engine::target_preparation::TargetProducer>, ExcelError> {
8909        use crate::engine::target_preparation::TargetProducer;
8910
8911        let request_id = self
8912            .active_evaluation_resource_request
8913            .as_ref()
8914            .map(|request| request.request_id);
8915        let mut roots = OrderedTargetProducers::with_capacity(targets.len())
8916            .map_err(|_| target_root_allocation_error(targets.len(), request_id))?;
8917        let resolve_region = |engine: &mut Self,
8918                              region: Region,
8919                              value_only: Option<CellRef>,
8920                              roots: &mut OrderedTargetProducers|
8921         -> Result<(), ExcelError> {
8922            let before = roots.len();
8923            for anchor in engine.graph.spill_anchors_in_region(
8924                region.sheet_id(),
8925                region.axis_ranges().0.query_bounds().0,
8926                region.axis_ranges().1.query_bounds().0,
8927                region.axis_ranges().0.query_bounds().1,
8928                region.axis_ranges().1.query_bounds().1,
8929            ) {
8930                roots
8931                    .push(TargetProducer::Legacy(anchor))
8932                    .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
8933            }
8934            for vertex in engine.graph.vertices_in_region(
8935                region.sheet_id(),
8936                region.axis_ranges().0.query_bounds().0,
8937                region.axis_ranges().0.query_bounds().1,
8938                region.axis_ranges().1.query_bounds().0,
8939                region.axis_ranges().1.query_bounds().1,
8940            ) {
8941                let vertex = engine
8942                    .graph
8943                    .get_cell_ref(vertex)
8944                    .and_then(|cell| engine.graph.spill_registry_anchor_for_cell(cell))
8945                    .unwrap_or(vertex);
8946                // `vertices_in_region` is a sheet-index query and a sheet index holds only
8947                // grid-addressed vertices, so a region can never yield a symbol: names,
8948                // tables and external sources have no position for a region to cover.
8949                // Symbol roots come from the by-name lookups below instead.
8950                if matches!(
8951                    engine.graph.get_vertex_kind(vertex),
8952                    VertexKind::FormulaScalar | VertexKind::FormulaArray
8953                ) {
8954                    roots
8955                        .push(TargetProducer::Legacy(vertex))
8956                        .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
8957                }
8958            }
8959            if roots.len() == before
8960                && let Some(cell) = value_only
8961            {
8962                roots
8963                    .push(TargetProducer::ValueOnly(cell))
8964                    .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
8965            }
8966            Ok(())
8967        };
8968
8969        for target in targets {
8970            match target {
8971                crate::engine::EvaluationTarget::Cell { sheet, row, col } => {
8972                    if *row == 0 || *col == 0 {
8973                        return Err(ExcelError::new(ExcelErrorKind::Ref)
8974                            .with_message("target cell coordinates are one-based"));
8975                    }
8976                    let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
8977                        ExcelError::new(ExcelErrorKind::Ref)
8978                            .with_message(format!("target sheet not found: {sheet}"))
8979                    })?;
8980                    let cell = CellRef::new(sheet_id, Coord::from_excel(*row, *col, true, true));
8981                    resolve_region(
8982                        self,
8983                        Region::point(sheet_id, *row - 1, *col - 1),
8984                        Some(cell),
8985                        &mut roots,
8986                    )?;
8987                }
8988                crate::engine::EvaluationTarget::Range(range) => {
8989                    let sheet_id = self.graph.sheet_id(&range.sheet).ok_or_else(|| {
8990                        ExcelError::new(ExcelErrorKind::Ref)
8991                            .with_message(format!("target sheet not found: {}", range.sheet))
8992                    })?;
8993                    resolve_region(
8994                        self,
8995                        Region::rect(
8996                            sheet_id,
8997                            range.start_row - 1,
8998                            range.end_row - 1,
8999                            range.start_col - 1,
9000                            range.end_col - 1,
9001                        ),
9002                        None,
9003                        &mut roots,
9004                    )?;
9005                }
9006                crate::engine::EvaluationTarget::Name { name, scope_sheet } => {
9007                    let scope = self.name_query_scope(scope_sheet.as_deref())?;
9008                    if let Some(entry) = self.graph.resolve_name_entry_in_scope(name, scope) {
9009                        roots
9010                            .push(TargetProducer::Symbol(entry.vertex))
9011                            .map_err(|_| {
9012                                target_root_allocation_error(roots.len() + 1, request_id)
9013                            })?;
9014                    }
9015                }
9016                crate::engine::EvaluationTarget::Table { name, .. } => {
9017                    if let Some(entry) = self.graph.resolve_table_entry(name) {
9018                        roots
9019                            .push(TargetProducer::Symbol(entry.vertex))
9020                            .map_err(|_| {
9021                                target_root_allocation_error(roots.len() + 1, request_id)
9022                            })?;
9023                    }
9024                }
9025            }
9026        }
9027        Ok(roots.into_vec())
9028    }
9029
9030    /// Transactionally prepare the complete ordinary staged demand closure for typed targets.
9031    ///
9032    /// This method prepares graph topology only. It does not evaluate target values.
9033    pub fn prepare_graph_for_targets(
9034        &mut self,
9035        targets: &[crate::engine::EvaluationTarget],
9036        options: crate::engine::TargetEvalOptions<'_>,
9037    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
9038        self.ensure_not_poisoned()?;
9039        let previous_budgets = self.evaluation_resource_budgets.clone();
9040        let diagnostics_len = self.formula_parse_diagnostics.len();
9041        let previous_report = self.last_formula_ingest_report.clone();
9042        if let Some(budgets) = options.budgets {
9043            self.evaluation_resource_budgets = budgets.clone();
9044        }
9045        let previous_graph_budget_override = self
9046            .graph
9047            .set_admission_budget_override(Some(self.evaluation_resource_budgets.clone()));
9048        // Hoist the call's cancellation onto the engine for its duration, so the
9049        // preparation checkpoints observe it. This is a standalone entry point, so
9050        // the previous value is restored rather than cleared.
9051        let previous_cancel = self.active_cancel_flag.take();
9052        self.active_cancel_flag = options.cancel.clone();
9053        let result = self.observe_evaluation_resource_request(
9054            EvaluationRequestKind::TargetPreparation,
9055            |engine| engine.prepare_graph_for_targets_unobserved(targets, &options),
9056        );
9057        self.active_cancel_flag = previous_cancel;
9058        self.graph
9059            .set_admission_budget_override(previous_graph_budget_override);
9060        self.evaluation_resource_budgets = previous_budgets;
9061        if result.is_err() {
9062            self.formula_parse_diagnostics.truncate(diagnostics_len);
9063            self.last_formula_ingest_report = previous_report;
9064        }
9065        result
9066    }
9067
9068    fn prepare_graph_for_targets_unobserved(
9069        &mut self,
9070        targets: &[crate::engine::EvaluationTarget],
9071        options: &crate::engine::TargetEvalOptions<'_>,
9072    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
9073        let scratch_checkpoint = self
9074            .active_resource_ledger
9075            .as_ref()
9076            .map_or(0, crate::engine::ResourceLedger::scratch_checkpoint);
9077        let result = self.prepare_graph_for_targets_transaction(targets, options);
9078        let release = self
9079            .active_resource_ledger
9080            .as_mut()
9081            .map_or(Ok(()), |ledger| {
9082                ledger.release_scratch_to(scratch_checkpoint)
9083            });
9084        match (result, release) {
9085            (result, Ok(())) => result,
9086            (Ok(_), Err(error)) | (Err(_), Err(error)) => Err(error.into_excel_error()),
9087        }
9088    }
9089
9090    fn prepare_graph_for_targets_transaction(
9091        &mut self,
9092        targets: &[crate::engine::EvaluationTarget],
9093        options: &crate::engine::TargetEvalOptions<'_>,
9094    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
9095        use crate::engine::{
9096            OpaqueReason, PreparationOutcome, PrepareScope, PreparedTargetGraphReport,
9097            TableSelection,
9098        };
9099
9100        self.target_preparation_checkpoint(options.deadline, 0)?;
9101        self.observe_function_semantic_epoch()?;
9102        let assumptions = self.preparation_revisions();
9103        let ledger_at_start = self
9104            .active_resource_ledger
9105            .as_ref()
9106            .map(|ledger| ledger.snapshot());
9107        let diagnostics_len = self.formula_parse_diagnostics.len();
9108        let report_len = self.last_formula_ingest_report.clone();
9109        let request_id = options.request_id.or_else(|| {
9110            self.active_evaluation_resource_request
9111                .as_ref()
9112                .map(|stats| stats.request_id)
9113        });
9114
9115        let mut scope = PrepareScope::Exact;
9116        let mut reasons = Vec::new();
9117        let mut regions = VecDeque::new();
9118        let mut deferred_shared_regions = VecDeque::new();
9119        let mut normalized = Vec::with_capacity(targets.len());
9120        let mut symbol_vertices = VecDeque::new();
9121        for target in targets {
9122            self.target_preparation_checkpoint(options.deadline, 1)?;
9123            match target {
9124                crate::engine::EvaluationTarget::Cell { sheet, row, col } => {
9125                    if *row == 0 || *col == 0 {
9126                        return Err(ExcelError::new(ExcelErrorKind::Ref)
9127                            .with_message("target cell coordinates are one-based"));
9128                    }
9129                    let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
9130                        ExcelError::new(ExcelErrorKind::Ref)
9131                            .with_message(format!("target sheet not found: {sheet}"))
9132                    })?;
9133                    regions.push_back(PreparationRegion {
9134                        sheet: sheet.clone(),
9135                        sheet_id,
9136                        start_row: *row,
9137                        start_col: *col,
9138                        end_row: *row,
9139                        end_col: *col,
9140                    });
9141                    normalized.push(target.clone());
9142                }
9143                crate::engine::EvaluationTarget::Range(range) => {
9144                    if range.start_row == 0
9145                        || range.start_col == 0
9146                        || range.end_row < range.start_row
9147                        || range.end_col < range.start_col
9148                    {
9149                        return Err(ExcelError::new(ExcelErrorKind::Ref)
9150                            .with_message("invalid target range"));
9151                    }
9152                    let sheet_id = self.graph.sheet_id(&range.sheet).ok_or_else(|| {
9153                        ExcelError::new(ExcelErrorKind::Ref)
9154                            .with_message(format!("target sheet not found: {}", range.sheet))
9155                    })?;
9156                    regions.push_back(PreparationRegion {
9157                        sheet: range.sheet.clone(),
9158                        sheet_id,
9159                        start_row: range.start_row,
9160                        start_col: range.start_col,
9161                        end_row: range.end_row,
9162                        end_col: range.end_col,
9163                    });
9164                    normalized.push(target.clone());
9165                }
9166                crate::engine::EvaluationTarget::Name { name, scope_sheet } => {
9167                    let name_scope = self.name_query_scope(scope_sheet.as_deref())?;
9168                    if let Some(entry) = self.graph.resolve_name_entry_in_scope(name, name_scope) {
9169                        symbol_vertices.push_back(entry.vertex);
9170                    } else {
9171                        Self::widen_target_preparation(
9172                            options.opaque_policy,
9173                            &mut scope,
9174                            &mut reasons,
9175                            OpaqueReason::UnresolvedName,
9176                        )?;
9177                    }
9178                    normalized.push(target.clone());
9179                }
9180                crate::engine::EvaluationTarget::Table { name, selection } => {
9181                    if let Some(entry) = self.graph.resolve_table_entry(name) {
9182                        let region = self.table_selection_region(entry, selection)?;
9183                        symbol_vertices.push_back(entry.vertex);
9184                        regions.push_back(region);
9185                    } else {
9186                        Self::widen_target_preparation(
9187                            options.opaque_policy,
9188                            &mut scope,
9189                            &mut reasons,
9190                            OpaqueReason::UnresolvedTable,
9191                        )?;
9192                    }
9193                    normalized.push(target.clone());
9194                }
9195            }
9196        }
9197
9198        let mut visited_regions = FxHashSet::default();
9199        let mut visited_vertices = FxHashSet::default();
9200        let mut selected = FxHashSet::default();
9201        let mut prepared = Vec::new();
9202        let mut pending_diagnostics = Vec::new();
9203        let mut planning_requests = BTreeSet::new();
9204        let mut selected_cells = Vec::new();
9205        let mut workbook_seeded = false;
9206        let mut sheet_scope_seeded = BTreeSet::new();
9207        let mut indexed_query_sheets = FxHashSet::default();
9208        let mut discovery_scratch_reserved = 0u64;
9209        let mut package_encountered = false;
9210        let mut selected_package_sheets = FxHashSet::default();
9211        let mut selected_package_points: BTreeMap<String, BTreeSet<(u32, u32)>> = BTreeMap::new();
9212        let mut prepared_packages: Vec<PreparedTargetSourcePackage> = Vec::new();
9213        let authoritative_with_ordinary = false;
9214        let has_unknown_package_sheet = self
9215            .staged_formula_index
9216            .package_sheets()
9217            .any(|sheet| self.graph.sheet_id(sheet).is_none());
9218
9219        loop {
9220            if let PrepareScope::Sheets(sheets) = &scope {
9221                for sheet in sheets.clone() {
9222                    if !sheet_scope_seeded.insert(sheet.clone()) {
9223                        continue;
9224                    }
9225                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
9226                        package_encountered = true;
9227                        Self::widen_target_preparation(
9228                            options.opaque_policy,
9229                            &mut scope,
9230                            &mut reasons,
9231                            OpaqueReason::UnsupportedSourceSemantics,
9232                        )?;
9233                        continue;
9234                    };
9235                    for lease in self.staged_formula_index.leases_for_sheet(&sheet) {
9236                        self.target_preparation_checkpoint(options.deadline, 1)?;
9237                        regions.push_back(PreparationRegion {
9238                            sheet: sheet.clone(),
9239                            sheet_id,
9240                            start_row: lease.row,
9241                            start_col: lease.col,
9242                            end_row: lease.row,
9243                            end_col: lease.col,
9244                        });
9245                    }
9246                    if self
9247                        .staged_formula_index
9248                        .package_lease_for_sheet(&sheet)
9249                        .is_some()
9250                    {
9251                        regions.push_back(PreparationRegion {
9252                            sheet: sheet.clone(),
9253                            sheet_id,
9254                            start_row: 1,
9255                            start_col: 1,
9256                            end_row: self.workbook_load_limits.max_sheet_rows,
9257                            end_col: self.workbook_load_limits.max_sheet_cols,
9258                        });
9259                    }
9260                }
9261            }
9262            if matches!(scope, PrepareScope::Workbook) && !workbook_seeded {
9263                workbook_seeded = true;
9264                for (sheet, lease) in self.staged_formula_index.all_leases() {
9265                    self.target_preparation_checkpoint(options.deadline, 1)?;
9266                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
9267                        package_encountered = true;
9268                        Self::widen_target_preparation(
9269                            options.opaque_policy,
9270                            &mut scope,
9271                            &mut reasons,
9272                            OpaqueReason::UnsupportedSourceSemantics,
9273                        )?;
9274                        continue;
9275                    };
9276                    regions.push_back(PreparationRegion {
9277                        sheet,
9278                        sheet_id,
9279                        start_row: lease.row,
9280                        start_col: lease.col,
9281                        end_row: lease.row,
9282                        end_col: lease.col,
9283                    });
9284                }
9285                let package_sheets = self
9286                    .staged_formula_index
9287                    .package_sheets()
9288                    .map(str::to_string)
9289                    .collect::<Vec<_>>();
9290                for sheet in package_sheets {
9291                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
9292                        package_encountered = true;
9293                        Self::widen_target_preparation(
9294                            options.opaque_policy,
9295                            &mut scope,
9296                            &mut reasons,
9297                            OpaqueReason::UnsupportedSourceSemantics,
9298                        )?;
9299                        continue;
9300                    };
9301                    regions.push_back(PreparationRegion {
9302                        sheet,
9303                        sheet_id,
9304                        start_row: 1,
9305                        start_col: 1,
9306                        end_row: self.workbook_load_limits.max_sheet_rows,
9307                        end_col: self.workbook_load_limits.max_sheet_cols,
9308                    });
9309                }
9310            }
9311
9312            // Finish known ordinary/name dependency discovery before expanding
9313            // partial shared demands. A queued SUM may complete those families.
9314            let allow_partial_shared = regions.is_empty() && symbol_vertices.is_empty();
9315            let next_region = if allow_partial_shared {
9316                deferred_shared_regions.pop_front()
9317            } else {
9318                regions.pop_front()
9319            };
9320            let Some(region) = next_region else {
9321                if let Some(vertex) = symbol_vertices.pop_front() {
9322                    self.target_preparation_checkpoint(options.deadline, 1)?;
9323                    if !visited_vertices.insert(vertex) || !self.graph.vertex_exists(vertex) {
9324                        continue;
9325                    }
9326                    let vertex_is_dynamic = self.graph.is_dynamic(vertex);
9327                    if let Some(ast) = self.graph.get_formula(vertex) {
9328                        let snapshot =
9329                            self.target_planning_snapshot(&ast, &mut planning_requests)?;
9330                        if let Some(reason) =
9331                            Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
9332                        {
9333                            return Err(Self::preparation_stale(
9334                                reason,
9335                                "target planning snapshot became stale during discovery",
9336                            ));
9337                        }
9338                        let opaque = self.opaque_reason_in_ast(&ast, &snapshot);
9339                        if let Some(reason) =
9340                            opaque.or(vertex_is_dynamic.then_some(OpaqueReason::DynamicReference))
9341                        {
9342                            if reason == OpaqueReason::DynamicReference
9343                                && Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot)
9344                            {
9345                                let sheet = self.graph.get_vertex_sheet_id(vertex);
9346                                let sheet = self.graph.sheet_name(sheet).to_string();
9347                                Self::widen_target_preparation_to_sheet(
9348                                    options.opaque_policy,
9349                                    &mut scope,
9350                                    &mut reasons,
9351                                    reason,
9352                                    &sheet,
9353                                )?;
9354                            } else {
9355                                Self::widen_target_preparation(
9356                                    options.opaque_policy,
9357                                    &mut scope,
9358                                    &mut reasons,
9359                                    reason,
9360                                )?;
9361                            }
9362                        }
9363                    } else if vertex_is_dynamic {
9364                        Self::widen_target_preparation(
9365                            options.opaque_policy,
9366                            &mut scope,
9367                            &mut reasons,
9368                            OpaqueReason::DynamicReference,
9369                        )?;
9370                    }
9371                    if let Some(anchor) = self
9372                        .graph
9373                        .get_cell_ref(vertex)
9374                        .and_then(|cell| self.graph.spill_registry_anchor_for_cell(cell))
9375                    {
9376                        symbol_vertices.push_back(anchor);
9377                    }
9378                    if let Some(cell) = self.graph.get_cell_ref(vertex) {
9379                        let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
9380                        regions.push_back(PreparationRegion {
9381                            sheet,
9382                            sheet_id: cell.sheet_id,
9383                            start_row: cell.coord.row() + 1,
9384                            start_col: cell.coord.col() + 1,
9385                            end_row: cell.coord.row() + 1,
9386                            end_col: cell.coord.col() + 1,
9387                        });
9388                    }
9389                    // The formula's direct precedents, from the authority:
9390                    // cells and ranges become regions, symbol rows (names,
9391                    // tables, sources) their vertices.
9392                    match self.graph.authority_vertex_precedents(vertex) {
9393                        Some(precedents) => {
9394                            for (sheet_id, rect) in precedents {
9395                                self.target_preparation_checkpoint(options.deadline, 1)?;
9396                                if sheet_id == crate::engine::authority::geom::SYMBOL_SHEET {
9397                                    for slot in rect.r0..=rect.r1 {
9398                                        if let Some(symbol) =
9399                                            self.graph.authority_host().symbols().vertex(slot)
9400                                        {
9401                                            symbol_vertices.push_back(symbol);
9402                                        }
9403                                    }
9404                                    continue;
9405                                }
9406                                let sheet = self.graph.sheet_name(sheet_id).to_string();
9407                                regions.push_back(PreparationRegion {
9408                                    sheet,
9409                                    sheet_id,
9410                                    start_row: rect.r0 + 1,
9411                                    start_col: rect.c0 + 1,
9412                                    end_row: (rect.r1 + 1)
9413                                        .min(self.workbook_load_limits.max_sheet_rows),
9414                                    end_col: (rect.c1 + 1)
9415                                        .min(self.workbook_load_limits.max_sheet_cols),
9416                                });
9417                            }
9418                        }
9419                        // The authority cannot answer (failed host): widen.
9420                        None => {
9421                            if Self::widen_target_preparation(
9422                                options.opaque_policy,
9423                                &mut scope,
9424                                &mut reasons,
9425                                OpaqueReason::UnresolvedCrossSheetBinding,
9426                            )? {
9427                                continue;
9428                            }
9429                        }
9430                    }
9431                    if let Some(name) = self.graph.named_range_by_vertex(vertex).cloned() {
9432                        match &name.definition {
9433                            NamedDefinition::Cell(cell) => regions.push_back(PreparationRegion {
9434                                sheet: self.graph.sheet_name(cell.sheet_id).to_string(),
9435                                sheet_id: cell.sheet_id,
9436                                start_row: cell.coord.row() + 1,
9437                                start_col: cell.coord.col() + 1,
9438                                end_row: cell.coord.row() + 1,
9439                                end_col: cell.coord.col() + 1,
9440                            }),
9441                            NamedDefinition::Range(range) => regions.push_back(PreparationRegion {
9442                                sheet: self.graph.sheet_name(range.start.sheet_id).to_string(),
9443                                sheet_id: range.start.sheet_id,
9444                                start_row: range.start.coord.row() + 1,
9445                                start_col: range.start.coord.col() + 1,
9446                                end_row: range.end.coord.row() + 1,
9447                                end_col: range.end.coord.col() + 1,
9448                            }),
9449                            NamedDefinition::Formula {
9450                                ast,
9451                                dependencies,
9452                                range_deps,
9453                            } => {
9454                                let snapshot =
9455                                    self.target_planning_snapshot(ast, &mut planning_requests)?;
9456                                if let Some(reason) = Self::target_planning_snapshot_stale_reason(
9457                                    &snapshot,
9458                                    &assumptions,
9459                                ) {
9460                                    return Err(Self::preparation_stale(
9461                                        reason,
9462                                        "target planning snapshot became stale during discovery",
9463                                    ));
9464                                }
9465                                if let Some(reason) = self.opaque_reason_in_ast(ast, &snapshot) {
9466                                    Self::widen_target_preparation(
9467                                        options.opaque_policy,
9468                                        &mut scope,
9469                                        &mut reasons,
9470                                        reason,
9471                                    )?;
9472                                }
9473                                for dependency in dependencies {
9474                                    self.target_preparation_checkpoint(options.deadline, 1)?;
9475                                    symbol_vertices.push_back(*dependency);
9476                                }
9477                                for range in range_deps {
9478                                    self.target_preparation_checkpoint(options.deadline, 1)?;
9479                                    // `Current` is the sheet this name's formula
9480                                    // was interpreted on, which is the sheet its
9481                                    // vertex is placed on -- the same derivation
9482                                    // the formula-vertex arm above uses. It is
9483                                    // never the workbook default sheet, and an
9484                                    // unresolvable `Name` widens instead of
9485                                    // silently landing on some other sheet.
9486                                    let context_sheet = self.graph.get_vertex_sheet_id(vertex);
9487                                    let Ok(sheet_id) =
9488                                        self.resolve_sheet_locator(&range.sheet, context_sheet)
9489                                    else {
9490                                        if Self::widen_target_preparation(
9491                                            options.opaque_policy,
9492                                            &mut scope,
9493                                            &mut reasons,
9494                                            OpaqueReason::UnresolvedCrossSheetBinding,
9495                                        )? {
9496                                            break;
9497                                        }
9498                                        continue;
9499                                    };
9500                                    regions.push_back(PreparationRegion {
9501                                        sheet: self.graph.sheet_name(sheet_id).to_string(),
9502                                        sheet_id,
9503                                        start_row: range
9504                                            .start_row
9505                                            .map_or(1, |bound| bound.index + 1),
9506                                        start_col: range
9507                                            .start_col
9508                                            .map_or(1, |bound| bound.index + 1),
9509                                        end_row: range.end_row.map_or(
9510                                            self.workbook_load_limits.max_sheet_rows,
9511                                            |bound| bound.index + 1,
9512                                        ),
9513                                        end_col: range.end_col.map_or(
9514                                            self.workbook_load_limits.max_sheet_cols,
9515                                            |bound| bound.index + 1,
9516                                        ),
9517                                    });
9518                                }
9519                            }
9520                            NamedDefinition::Literal(_) => {}
9521                        }
9522                    }
9523                    if let Some(table) = self.graph.table_by_vertex(vertex) {
9524                        regions
9525                            .push_back(self.table_selection_region(table, &TableSelection::Whole)?);
9526                    }
9527                    continue;
9528                }
9529                break;
9530            };
9531
9532            self.target_preparation_checkpoint(options.deadline, 1)?;
9533            if !visited_regions.insert(region.clone()) && !allow_partial_shared {
9534                continue;
9535            }
9536            if let PrepareScope::Sheets(sheets) = &mut scope
9537                && !sheets.iter().any(|sheet| sheet == &region.sheet)
9538            {
9539                sheets.push(region.sheet.clone());
9540                sheets.sort();
9541            }
9542            let package_match = self.staged_formula_index.package_for_region(
9543                &region.sheet,
9544                region.start_row,
9545                region.start_col,
9546                region.end_row,
9547                region.end_col,
9548            );
9549            let package_lease = match package_match {
9550                Some(Ok(lease)) => Some(lease),
9551                Some(Err(()))
9552                    if region.start_row == 1
9553                        && region.start_col == 1
9554                        && region.end_row == self.workbook_load_limits.max_sheet_rows
9555                        && region.end_col == self.workbook_load_limits.max_sheet_cols =>
9556                {
9557                    self.staged_formula_index
9558                        .package_lease_for_sheet(&region.sheet)
9559                }
9560                Some(Err(())) => {
9561                    Self::widen_target_preparation(
9562                        options.opaque_policy,
9563                        &mut scope,
9564                        &mut reasons,
9565                        OpaqueReason::DeferredSourcePackage,
9566                    )?;
9567                    None
9568                }
9569                None => None,
9570            };
9571            let compatibility_before_package_replay = package_lease.is_some()
9572                && (authoritative_with_ordinary || has_unknown_package_sheet);
9573            let package_lease = if compatibility_before_package_replay {
9574                package_encountered = true;
9575                Self::widen_target_preparation(
9576                    options.opaque_policy,
9577                    &mut scope,
9578                    &mut reasons,
9579                    OpaqueReason::UnsupportedSourceSemantics,
9580                )?;
9581                None
9582            } else {
9583                package_lease
9584            };
9585            if let Some(package_lease) = package_lease
9586                && !selected_package_sheets.contains(&region.sheet)
9587            {
9588                self.target_preparation_checkpoint(options.deadline, 1)?;
9589                let mut points = self.staged_formula_index.package_points_in_region(
9590                    &region.sheet,
9591                    region.start_row,
9592                    region.start_col,
9593                    region.end_row,
9594                    region.end_col,
9595                );
9596                if let Some(selected) = selected_package_points.get(&region.sheet) {
9597                    points.retain(|point| !selected.contains(point));
9598                }
9599
9600                if let Some(selected) = selected_package_points.get(&region.sheet) {
9601                    points.retain(|point| !selected.contains(point));
9602                }
9603                let permit_partial = allow_partial_shared
9604                    || (points.len() == 1
9605                        && regions.is_empty()
9606                        && symbol_vertices.is_empty()
9607                        && deferred_shared_regions.is_empty());
9608                let partial = self.prepare_target_exact_source_selection(
9609                    &region.sheet,
9610                    package_lease,
9611                    points,
9612                    selected_package_points
9613                        .get(&region.sheet)
9614                        .unwrap_or(&BTreeSet::new()),
9615                    permit_partial,
9616                    options.deadline,
9617                    &mut discovery_scratch_reserved,
9618                )?;
9619                let mut package = if let Some(mut package) = partial {
9620                    if package.deferred_shared {
9621                        deferred_shared_regions.push_back(region.clone());
9622                    }
9623                    // A later range can complete a family touched earlier in this
9624                    // request. Retire only that family's earlier legacy proposals;
9625                    // nothing has been published and its dependencies remain demanded.
9626                    if !package.direct_domains.is_empty() {
9627                        for prior in prepared_packages
9628                            .iter_mut()
9629                            .filter(|prior| prior.sheet == region.sheet)
9630                        {
9631                            prior
9632                                .replay_records
9633                                .retain(|record| !package.direct_contains(record.row, record.col));
9634                            prior
9635                                .legacy
9636                                .retain(|(row, col, _, _)| !package.direct_contains(*row, *col));
9637                            if let Some(points) = prior.selected_points.as_mut() {
9638                                points.retain(|&(row, col)| !package.direct_contains(row, col));
9639                            }
9640                        }
9641                    }
9642                    if selected_package_points.contains_key(&region.sheet) {
9643                        package.source_report = Default::default();
9644                    }
9645                    let points = package.selected_points.as_ref().unwrap();
9646                    if !points.is_empty() {
9647                        selected_package_points
9648                            .entry(region.sheet.clone())
9649                            .or_default()
9650                            .extend(points.iter().copied());
9651                    }
9652                    package
9653                } else {
9654                    selected_package_sheets.insert(region.sheet.clone());
9655                    self.prepare_target_source_package(
9656                        &region.sheet,
9657                        package_lease,
9658                        options.deadline,
9659                    )?
9660                };
9661                if package
9662                    .selected_points
9663                    .as_ref()
9664                    .is_none_or(|points| !points.is_empty())
9665                {
9666                    let mut final_fallback = BTreeMap::new();
9667                    for record in package.fallback_records() {
9668                        final_fallback.insert((record.row, record.col), record.clone());
9669                    }
9670                    let batch = self.formula_batch_from_exact_replay(
9671                        &region.sheet,
9672                        final_fallback.into_values(),
9673                    )?;
9674                    for record in batch.formulas {
9675                        self.target_preparation_checkpoint(options.deadline, 1)?;
9676                        let ast = self
9677                            .graph
9678                            .data_store()
9679                            .retrieve_ast(record.ast_id, self.graph.sheet_reg())
9680                            .ok_or_else(|| {
9681                                ExcelError::new(ExcelErrorKind::Value)
9682                                    .with_message("target fallback AST is unavailable")
9683                            })?;
9684                        let snapshot =
9685                            self.target_planning_snapshot(&ast, &mut planning_requests)?;
9686                        if let Some(reason) =
9687                            Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
9688                        {
9689                            return Err(Self::preparation_stale(
9690                                reason,
9691                                "target fallback planning snapshot became stale during discovery",
9692                            ));
9693                        }
9694                        let proven_sheet_local_dynamic =
9695                            Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot);
9696                        if let Some(reason) = self.opaque_reason_in_ast(&ast, &snapshot) {
9697                            if reason == OpaqueReason::DynamicReference
9698                                && proven_sheet_local_dynamic
9699                            {
9700                                Self::widen_target_preparation_to_sheet(
9701                                    options.opaque_policy,
9702                                    &mut scope,
9703                                    &mut reasons,
9704                                    reason,
9705                                    &region.sheet,
9706                                )?;
9707                            } else {
9708                                Self::widen_target_preparation(
9709                                    options.opaque_policy,
9710                                    &mut scope,
9711                                    &mut reasons,
9712                                    reason,
9713                                )?;
9714                            }
9715                        }
9716                        let placement = CellRef::new(
9717                            package.sheet_id,
9718                            Coord::from_excel(record.row, record.col, true, true),
9719                        );
9720                        let ingested = self
9721                            .graph
9722                            .ingest_pipeline(&snapshot)
9723                            .enable_function_semantics()
9724                            .ingest_formula(
9725                                FormulaAstInput::RawArena(record.ast_id),
9726                                placement,
9727                                record.formula_text,
9728                            )?;
9729                        if ingested.dep_plan.dynamic {
9730                            if proven_sheet_local_dynamic {
9731                                Self::widen_target_preparation_to_sheet(
9732                                    options.opaque_policy,
9733                                    &mut scope,
9734                                    &mut reasons,
9735                                    OpaqueReason::DynamicReference,
9736                                    &region.sheet,
9737                                )?;
9738                            } else {
9739                                Self::widen_target_preparation(
9740                                    options.opaque_policy,
9741                                    &mut scope,
9742                                    &mut reasons,
9743                                    OpaqueReason::DynamicReference,
9744                                )?;
9745                            }
9746                        }
9747                        for dep in &ingested.dep_plan.direct_cell_deps {
9748                            self.target_preparation_checkpoint(options.deadline, 1)?;
9749                            regions.push_back(PreparationRegion {
9750                                sheet: self.graph.sheet_name(dep.sheet_id).to_string(),
9751                                sheet_id: dep.sheet_id,
9752                                start_row: dep.coord.row().saturating_add(1),
9753                                start_col: dep.coord.col().saturating_add(1),
9754                                end_row: dep.coord.row().saturating_add(1),
9755                                end_col: dep.coord.col().saturating_add(1),
9756                            });
9757                        }
9758                        for range in &ingested.dep_plan.range_deps {
9759                            self.target_preparation_checkpoint(options.deadline, 1)?;
9760                            // `Current` is the sheet the staged package's formula
9761                            // lives on.
9762                            let Ok(dependency_sheet) =
9763                                self.resolve_sheet_locator(&range.sheet, package.sheet_id)
9764                            else {
9765                                Self::widen_target_preparation(
9766                                    options.opaque_policy,
9767                                    &mut scope,
9768                                    &mut reasons,
9769                                    OpaqueReason::UnresolvedCrossSheetBinding,
9770                                )?;
9771                                continue;
9772                            };
9773                            regions.push_back(PreparationRegion {
9774                                sheet: self.graph.sheet_name(dependency_sheet).to_string(),
9775                                sheet_id: dependency_sheet,
9776                                start_row: range.start_row.map_or(1, |bound| bound.index + 1),
9777                                start_col: range.start_col.map_or(1, |bound| bound.index + 1),
9778                                end_row: range
9779                                    .end_row
9780                                    .map_or(self.workbook_load_limits.max_sheet_rows, |bound| {
9781                                        bound.index + 1
9782                                    }),
9783                                end_col: range
9784                                    .end_col
9785                                    .map_or(self.workbook_load_limits.max_sheet_cols, |bound| {
9786                                        bound.index + 1
9787                                    }),
9788                            });
9789                        }
9790                        for name in ingested
9791                            .dep_plan
9792                            .resolved_named_refs
9793                            .iter()
9794                            .chain(&ingested.dep_plan.named_refs)
9795                        {
9796                            self.target_preparation_checkpoint(options.deadline, 1)?;
9797                            if let Some(entry) =
9798                                self.graph.resolve_name_entry(name, package.sheet_id)
9799                            {
9800                                symbol_vertices.push_back(entry.vertex);
9801                            } else if self.graph.resolve_source_scalar_entry(name).is_none()
9802                                && self.graph.resolve_source_table_entry(name).is_none()
9803                            {
9804                                Self::widen_target_preparation(
9805                                    options.opaque_policy,
9806                                    &mut scope,
9807                                    &mut reasons,
9808                                    OpaqueReason::UnresolvedName,
9809                                )?;
9810                            }
9811                        }
9812                        for table in &ingested.dep_plan.table_refs {
9813                            self.target_preparation_checkpoint(options.deadline, 1)?;
9814                            if let Some(entry) = self.graph.resolve_table_entry(table) {
9815                                symbol_vertices.push_back(entry.vertex);
9816                            } else if self.graph.resolve_source_table_entry(table).is_none() {
9817                                Self::widen_target_preparation(
9818                                    options.opaque_policy,
9819                                    &mut scope,
9820                                    &mut reasons,
9821                                    OpaqueReason::UnresolvedTable,
9822                                )?;
9823                            }
9824                        }
9825                        package.legacy.push((
9826                            record.row,
9827                            record.col,
9828                            ingested.ast_id,
9829                            ingested.dep_plan,
9830                        ));
9831                    }
9832                    prepared_packages.push(package);
9833                }
9834            }
9835            let leases = self.staged_formula_index.leases_in_region(
9836                &region.sheet,
9837                region.start_row,
9838                region.start_col,
9839                region.end_row,
9840                region.end_col,
9841            );
9842            for lease in leases {
9843                self.target_preparation_checkpoint(options.deadline, 1)?;
9844                let sheet_id = self.graph.sheet_id(&region.sheet).ok_or_else(|| {
9845                    ExcelError::new(ExcelErrorKind::Ref)
9846                        .with_message(format!("staged formula sheet not found: {}", region.sheet))
9847                })?;
9848                let key = (region.sheet.clone(), lease.row, lease.col, lease.generation);
9849                if !selected.insert(key) {
9850                    continue;
9851                }
9852                let text = self
9853                    .staged_formulas
9854                    .get(&region.sheet)
9855                    .and_then(|sheet| sheet.get_ordinary(lease.row, lease.col))
9856                    .ok_or_else(|| {
9857                        ExcelError::new(ExcelErrorKind::Value)
9858                            .with_message("staged formula index is stale")
9859                    })?
9860                    .to_string();
9861                let formula = if text.starts_with('=') {
9862                    text.clone()
9863                } else {
9864                    format!("={text}")
9865                };
9866                self.target_preparation_checkpoint(options.deadline, 1)?;
9867                let ast = match formualizer_parse::parser::parse(&formula) {
9868                    Ok(ast) => ast,
9869                    Err(error) => {
9870                        if self.config.formula_parse_policy == FormulaParsePolicy::Strict {
9871                            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
9872                                format!(
9873                                    "Formula parse error at {}!{}{}: {error}",
9874                                    region.sheet,
9875                                    col_letters_from_1based(lease.col)
9876                                        .unwrap_or_else(|_| "?".to_string()),
9877                                    lease.row
9878                                ),
9879                            ));
9880                        }
9881                        pending_diagnostics.push(FormulaParseDiagnostic {
9882                            sheet: region.sheet.clone(),
9883                            row: lease.row,
9884                            col: lease.col,
9885                            formula: formula.clone(),
9886                            message: error.to_string(),
9887                            policy: self.config.formula_parse_policy,
9888                        });
9889                        match self.config.formula_parse_policy {
9890                            FormulaParsePolicy::KeepCachedValue => {
9891                                selected_cells.push(
9892                                    formualizer_common::RangeAddress::new(
9893                                        region.sheet.clone(),
9894                                        lease.row,
9895                                        lease.col,
9896                                        lease.row,
9897                                        lease.col,
9898                                    )
9899                                    .expect("selected staged coordinates are valid"),
9900                                );
9901                                prepared.push(PreparedOrdinaryStagedFormula {
9902                                    sheet: region.sheet.clone(),
9903                                    sheet_id,
9904                                    lease,
9905                                    ast_id: None,
9906                                    plan: None,
9907                                });
9908                                continue;
9909                            }
9910                            FormulaParsePolicy::AsText => ASTNode::new(
9911                                ASTNodeType::Literal(LiteralValue::Text(formula.clone())),
9912                                None,
9913                            ),
9914                            FormulaParsePolicy::CoerceToError => ASTNode::new(
9915                                ASTNodeType::Literal(LiteralValue::Error(
9916                                    ExcelError::new(ExcelErrorKind::Error)
9917                                        .with_message(format!("Malformed formula: {error}")),
9918                                )),
9919                                None,
9920                            ),
9921                            FormulaParsePolicy::Strict => unreachable!(),
9922                        }
9923                    }
9924                };
9925                self.target_preparation_checkpoint(options.deadline, 1)?;
9926                let snapshot = self.target_planning_snapshot(&ast, &mut planning_requests)?;
9927                self.target_preparation_checkpoint(options.deadline, 1)?;
9928                if let Some(reason) =
9929                    Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
9930                {
9931                    return Err(Self::preparation_stale(
9932                        reason,
9933                        "target planning snapshot became stale during discovery",
9934                    ));
9935                }
9936                if let Some(reason) = self.opaque_reason_in_ast(&ast, &snapshot) {
9937                    if reason == OpaqueReason::DynamicReference
9938                        && Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot)
9939                    {
9940                        Self::widen_target_preparation_to_sheet(
9941                            options.opaque_policy,
9942                            &mut scope,
9943                            &mut reasons,
9944                            reason,
9945                            &region.sheet,
9946                        )?;
9947                    } else {
9948                        Self::widen_target_preparation(
9949                            options.opaque_policy,
9950                            &mut scope,
9951                            &mut reasons,
9952                            reason,
9953                        )?;
9954                    }
9955                }
9956                let proven_sheet_local_dynamic =
9957                    Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot);
9958                let placement = CellRef::new(
9959                    sheet_id,
9960                    Coord::from_excel(lease.row, lease.col, true, true),
9961                );
9962                let ingested = self.graph.ingest_pipeline(&snapshot).ingest_formula(
9963                    FormulaAstInput::Tree(ast),
9964                    placement,
9965                    Some(Arc::from(formula)),
9966                )?;
9967                self.target_preparation_checkpoint(options.deadline, 1)?;
9968                if ingested.dep_plan.dynamic {
9969                    if proven_sheet_local_dynamic {
9970                        Self::widen_target_preparation_to_sheet(
9971                            options.opaque_policy,
9972                            &mut scope,
9973                            &mut reasons,
9974                            OpaqueReason::DynamicReference,
9975                            &region.sheet,
9976                        )?;
9977                    } else {
9978                        Self::widen_target_preparation(
9979                            options.opaque_policy,
9980                            &mut scope,
9981                            &mut reasons,
9982                            OpaqueReason::DynamicReference,
9983                        )?;
9984                    }
9985                }
9986                for dep in &ingested.dep_plan.direct_cell_deps {
9987                    self.target_preparation_checkpoint(options.deadline, 1)?;
9988                    regions.push_back(PreparationRegion {
9989                        sheet: self.graph.sheet_name(dep.sheet_id).to_string(),
9990                        sheet_id: dep.sheet_id,
9991                        start_row: dep.coord.row() + 1,
9992                        start_col: dep.coord.col() + 1,
9993                        end_row: dep.coord.row() + 1,
9994                        end_col: dep.coord.col() + 1,
9995                    });
9996                }
9997                for range in &ingested.dep_plan.range_deps {
9998                    self.target_preparation_checkpoint(options.deadline, 1)?;
9999                    // `Current` is the sheet the staged formula lives on.
10000                    let Ok(dependency_sheet) = self.resolve_sheet_locator(&range.sheet, sheet_id)
10001                    else {
10002                        Self::widen_target_preparation(
10003                            options.opaque_policy,
10004                            &mut scope,
10005                            &mut reasons,
10006                            OpaqueReason::UnresolvedCrossSheetBinding,
10007                        )?;
10008                        continue;
10009                    };
10010                    regions.push_back(PreparationRegion {
10011                        sheet: self.graph.sheet_name(dependency_sheet).to_string(),
10012                        sheet_id: dependency_sheet,
10013                        start_row: range.start_row.map_or(1, |bound| bound.index + 1),
10014                        start_col: range.start_col.map_or(1, |bound| bound.index + 1),
10015                        end_row: range
10016                            .end_row
10017                            .map_or(self.workbook_load_limits.max_sheet_rows, |bound| {
10018                                bound.index + 1
10019                            }),
10020                        end_col: range
10021                            .end_col
10022                            .map_or(self.workbook_load_limits.max_sheet_cols, |bound| {
10023                                bound.index + 1
10024                            }),
10025                    });
10026                }
10027                for name in ingested
10028                    .dep_plan
10029                    .resolved_named_refs
10030                    .iter()
10031                    .chain(&ingested.dep_plan.named_refs)
10032                {
10033                    self.target_preparation_checkpoint(options.deadline, 1)?;
10034                    if let Some(entry) = self.graph.resolve_name_entry(name, sheet_id) {
10035                        symbol_vertices.push_back(entry.vertex);
10036                    } else if self.graph.resolve_source_scalar_entry(name).is_none()
10037                        && self.graph.resolve_source_table_entry(name).is_none()
10038                    {
10039                        Self::widen_target_preparation(
10040                            options.opaque_policy,
10041                            &mut scope,
10042                            &mut reasons,
10043                            OpaqueReason::UnresolvedName,
10044                        )?;
10045                    }
10046                }
10047                for table in &ingested.dep_plan.table_refs {
10048                    self.target_preparation_checkpoint(options.deadline, 1)?;
10049                    if let Some(entry) = self.graph.resolve_table_entry(table) {
10050                        symbol_vertices.push_back(entry.vertex);
10051                    } else if self.graph.resolve_source_table_entry(table).is_none() {
10052                        Self::widen_target_preparation(
10053                            options.opaque_policy,
10054                            &mut scope,
10055                            &mut reasons,
10056                            OpaqueReason::UnresolvedTable,
10057                        )?;
10058                    }
10059                }
10060                selected_cells.push(
10061                    formualizer_common::RangeAddress::new(
10062                        region.sheet.clone(),
10063                        lease.row,
10064                        lease.col,
10065                        lease.row,
10066                        lease.col,
10067                    )
10068                    .expect("selected staged coordinates are valid"),
10069                );
10070                prepared.push(PreparedOrdinaryStagedFormula {
10071                    sheet: region.sheet.clone(),
10072                    sheet_id,
10073                    lease,
10074                    ast_id: Some(ingested.ast_id),
10075                    plan: Some(ingested.dep_plan),
10076                });
10077            }
10078
10079            if indexed_query_sheets.insert(region.sheet_id) {
10080                self.graph.prepare_sheet_index_for_query(region.sheet_id);
10081                let bytes = (self.graph.sheet_index_vertex_count(region.sheet_id) as u64)
10082                    .saturating_mul(32);
10083                self.reserve_graph_source_scratch(bytes)?;
10084                discovery_scratch_reserved = discovery_scratch_reserved.saturating_add(bytes);
10085            }
10086            let spill_anchors = self.graph.spill_anchors_in_region(
10087                region.sheet_id,
10088                region.start_row - 1,
10089                region.start_col - 1,
10090                region.end_row - 1,
10091                region.end_col - 1,
10092            );
10093            for anchor in spill_anchors {
10094                self.target_preparation_checkpoint(options.deadline, 1)?;
10095                symbol_vertices.push_back(anchor);
10096            }
10097            let vertices = self.graph.vertices_in_region(
10098                region.sheet_id,
10099                region.start_row - 1,
10100                region.end_row - 1,
10101                region.start_col - 1,
10102                region.end_col - 1,
10103            );
10104            for vertex in vertices {
10105                self.target_preparation_checkpoint(options.deadline, 1)?;
10106                symbol_vertices.push_back(vertex);
10107            }
10108        }
10109
10110        #[cfg(test)]
10111        self.target_preparation_fault(
10112            crate::engine::target_preparation::TargetPreparationFault::AfterDiscovery,
10113        )?;
10114
10115        if package_encountered {
10116            Self::widen_target_preparation(
10117                options.opaque_policy,
10118                &mut scope,
10119                &mut reasons,
10120                OpaqueReason::UnsupportedSourceSemantics,
10121            )?;
10122            self.target_preparation_checkpoint(options.deadline, 0)?;
10123            let selected_count = self.staged_formula_count();
10124            let selected_packages = self
10125                .staged_formulas
10126                .values()
10127                .filter_map(|staged| staged.deferred_package.as_ref())
10128                .map(|package| package.families.len() + package.partitioned_families.len())
10129                .sum();
10130            self.formula_parse_diagnostics.truncate(diagnostics_len);
10131            self.last_formula_ingest_report = report_len;
10132            #[cfg(test)]
10133            if let Some(hook) = self.before_target_preparation_commit_hook.take() {
10134                hook();
10135            }
10136            self.target_preparation_checkpoint(options.deadline, 0)?;
10137            #[cfg(test)]
10138            self.target_preparation_fault(
10139                crate::engine::target_preparation::TargetPreparationFault::FinalRevisionValidation,
10140            )?;
10141            let current_revisions = self.preparation_revisions();
10142            if let Some(reason) = Self::preparation_revision_stale_reason(
10143                &assumptions,
10144                &current_revisions,
10145                &planning_requests,
10146                true,
10147            ) {
10148                return Err(Self::preparation_stale(
10149                    reason,
10150                    "target compatibility preparation plan is stale",
10151                ));
10152            }
10153            #[cfg(test)]
10154            self.target_preparation_fault(
10155                crate::engine::target_preparation::TargetPreparationFault::FinalGraphValidation,
10156            )?;
10157            let commit_work_before = self
10158                .active_resource_ledger
10159                .as_ref()
10160                .map_or(0, |ledger| ledger.snapshot().work_charged);
10161            let commit_started = crate::instant::FzInstant::now();
10162            self.build_graph_all_unobserved()?;
10163            let commit_window = commit_started.elapsed();
10164            let ledger_after = self
10165                .active_resource_ledger
10166                .as_ref()
10167                .map(|ledger| ledger.snapshot());
10168            let actual_commit_work = ledger_after
10169                .map_or(0, |snapshot| snapshot.work_charged)
10170                .saturating_sub(commit_work_before);
10171            let observed_scratch_bytes = ledger_after
10172                .map_or(0, |snapshot| snapshot.scratch_peak)
10173                .saturating_sub(ledger_at_start.map_or(0, |snapshot| snapshot.scratch_current));
10174            let revisions = assumptions.clone();
10175            let report = PreparedTargetGraphReport {
10176                request_id: request_id.unwrap_or_default(),
10177                requested_targets: targets.len(),
10178                normalized_regions: visited_regions.len(),
10179                normalized_target_list: normalized,
10180                selected_staged_cells: selected_count,
10181                selected_source_families: selected_packages,
10182                retained_staged_cells: self.staged_formula_count(),
10183                selected_cells,
10184                retained_cells: Vec::new(),
10185                widened_scope: PrepareScope::Workbook,
10186                widening_reasons: reasons,
10187                revisions,
10188                commit_window,
10189                estimated_scratch_bytes: discovery_scratch_reserved
10190                    .saturating_add((selected_count as u64).saturating_mul(256)),
10191                observed_scratch_bytes,
10192                estimated_commit_work: selected_count as u64,
10193                actual_commit_work,
10194                outcome: PreparationOutcome::CompatibilityPrepared,
10195            };
10196            self.observe_target_preparation_report(&report);
10197            return Ok(report);
10198        }
10199
10200        prepared.sort_by_key(|formula| formula.lease.insertion_order);
10201        for package in &prepared_packages {
10202            if let Some(points) = &package.selected_points {
10203                selected_cells.extend(points.iter().filter_map(|&(row, col)| {
10204                    formualizer_common::RangeAddress::new(&package.sheet, row, col, row, col).ok()
10205                }));
10206                continue;
10207            }
10208            selected_cells.extend(package.replay_records.iter().filter_map(|record| {
10209                formualizer_common::RangeAddress::new(
10210                    package.sheet.clone(),
10211                    record.row,
10212                    record.col,
10213                    record.row,
10214                    record.col,
10215                )
10216                .ok()
10217            }));
10218        }
10219        let (legacy_graph, planned_formula_count) =
10220            self.prepare_target_combined_legacy_graph(&prepared_packages, &prepared)?;
10221        let new_vertices = legacy_graph.new_vertex_count();
10222        let new_edges = legacy_graph.planned_edge_count().ok_or_else(|| {
10223            ExcelError::new(ExcelErrorKind::NImpl).with_message("target graph edge count overflow")
10224        })?;
10225        let removed_edges = legacy_graph.removed_edge_count().ok_or_else(|| {
10226            ExcelError::new(ExcelErrorKind::NImpl).with_message("target graph edge count overflow")
10227        })?;
10228        let current = self.graph.baseline_stats();
10229        let final_vertices = current
10230            .graph_vertex_count
10231            .checked_add(new_vertices)
10232            .ok_or_else(|| {
10233                crate::engine::ResourceLedgerError::Exhausted(
10234                    formualizer_common::ResourceExhaustionDetail {
10235                        reason: formualizer_common::ResourceExhaustionReason::ArithmeticOverflow,
10236                        limit: u64::MAX,
10237                        observed: u64::MAX,
10238                        request_id,
10239                    },
10240                )
10241                .into_excel_error()
10242            })?;
10243        let final_edges = current
10244            .graph_edge_count
10245            .checked_sub(removed_edges)
10246            .and_then(|count| count.checked_add(new_edges))
10247            .ok_or_else(|| {
10248                crate::engine::ResourceLedgerError::Exhausted(
10249                    formualizer_common::ResourceExhaustionDetail {
10250                        reason: formualizer_common::ResourceExhaustionReason::ArithmeticOverflow,
10251                        limit: u64::MAX,
10252                        observed: u64::MAX,
10253                        request_id,
10254                    },
10255                )
10256                .into_excel_error()
10257            })?;
10258        #[cfg(test)]
10259        self.target_preparation_fault(
10260            crate::engine::target_preparation::TargetPreparationFault::Admission,
10261        )?;
10262        let resource = |reason, limit: u64, observed: u64| {
10263            crate::engine::ResourceLedgerError::Exhausted(
10264                formualizer_common::ResourceExhaustionDetail {
10265                    reason,
10266                    limit,
10267                    observed,
10268                    request_id,
10269                },
10270            )
10271            .into_excel_error()
10272        };
10273        let admission = crate::engine::resource_ledger::GraphAdmission {
10274            final_vertices,
10275            final_edges,
10276            materialization_cells: planned_formula_count as u64,
10277            added_vertices: new_vertices,
10278            added_edges: new_edges,
10279        };
10280        let materialized_bytes = admission
10281            .materialized_graph_bytes()
10282            .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
10283        if let Err(error) = self.preflight_graph_admission(admission) {
10284            if let formualizer_common::ExcelErrorExtra::Resource { detail } = error.extra() {
10285                self.observe_target_admission_failure(detail.reason);
10286            }
10287            return Err(error);
10288        }
10289        let selected_package_records = prepared_packages
10290            .iter()
10291            .map(|package| package.replay_records.len() as u64)
10292            .sum::<u64>();
10293        let planned_working_bytes = (prepared.len() as u64)
10294            .saturating_add(selected_package_records)
10295            .saturating_mul(256)
10296            .saturating_add((visited_regions.len() as u64).saturating_mul(128))
10297            .saturating_add((visited_vertices.len() as u64).saturating_mul(32))
10298            .saturating_add((selected.len() as u64).saturating_mul(96))
10299            .saturating_add(materialized_bytes);
10300        let residual_scratch = selected_package_points
10301            .iter()
10302            .map(|(sheet, points)| {
10303                let source = self
10304                    .staged_formulas
10305                    .get(sheet)
10306                    .unwrap()
10307                    .deferred_package
10308                    .as_ref()
10309                    .unwrap();
10310                (source.families.len() as u64)
10311                    .saturating_mul(256)
10312                    .saturating_add(
10313                        source
10314                            .partitioned_families
10315                            .iter()
10316                            .map(|family| {
10317                                256u64
10318                                    .saturating_add(family.fragments.len() as u64 * 32)
10319                                    .saturating_add(family.legacy_members.len() as u64 * 32)
10320                            })
10321                            .sum::<u64>(),
10322                    )
10323                    // Direct complete domains need coordinate suppression, not
10324                    // a per-member residual split/proof or legacy AST reserve.
10325                    .saturating_add(points.len() as u64 * 32)
10326                    .saturating_add(
10327                        prepared_packages
10328                            .iter()
10329                            .filter(|package| &package.sheet == sheet)
10330                            .map(|package| package.replay_records.len() as u64 * 480)
10331                            .sum::<u64>(),
10332                    )
10333            })
10334            .sum::<u64>();
10335        let scratch_bytes = discovery_scratch_reserved
10336            .saturating_add(planned_working_bytes)
10337            .saturating_add(residual_scratch);
10338        let remaining_scratch = scratch_bytes.saturating_sub(discovery_scratch_reserved);
10339        if let Err(error) = self.reserve_graph_source_scratch(remaining_scratch) {
10340            self.observe_target_admission_failure(
10341                formualizer_common::ResourceExhaustionReason::ScratchMemory,
10342            );
10343            return Err(error);
10344        }
10345
10346        let mut residual_sources = BTreeMap::new();
10347        let mut residual_owners = BTreeMap::new();
10348        for (sheet, points) in &selected_package_points {
10349            let selected: BTreeMap<_, BTreeSet<_>> = prepared_packages
10350                .iter()
10351                .filter(|package| &package.sheet == sheet)
10352                .flat_map(|package| &package.replay_records)
10353                .filter_map(|record| {
10354                    record.partition_owner.or(record.family).map(|owner| {
10355                        (
10356                            owner,
10357                            crate::engine::SourceCoord {
10358                                row: record.row - 1,
10359                                col: record.col - 1,
10360                            },
10361                        )
10362                    })
10363                })
10364                .fold(BTreeMap::new(), |mut map, (owner, coord)| {
10365                    map.entry(owner).or_default().insert(coord);
10366                    map
10367                });
10368            let metadata_work = self
10369                .staged_formulas
10370                .get(sheet)
10371                .unwrap()
10372                .deferred_package
10373                .as_ref()
10374                .map_or(0, |source| {
10375                    source.families.len() as u64
10376                        + source
10377                            .partitioned_families
10378                            .iter()
10379                            .map(|family| {
10380                                1 + family.fragments.len() as u64 * family.fragments.len() as u64
10381                                    + family.legacy_members.len() as u64
10382                            })
10383                            .sum::<u64>()
10384                });
10385            let split_work = selected
10386                .values()
10387                .map(|points| points.len() as u64 * 128)
10388                .sum::<u64>();
10389            self.target_preparation_checkpoint(
10390                options.deadline,
10391                metadata_work
10392                    .saturating_add(split_work)
10393                    .saturating_add(points.len() as u64),
10394            )?;
10395            let source = self
10396                .staged_formulas
10397                .get(sheet)
10398                .unwrap()
10399                .deferred_package
10400                .as_ref()
10401                .unwrap();
10402            let complete: BTreeSet<_> = prepared_packages
10403                .iter()
10404                .filter(|package| &package.sheet == sheet)
10405                .flat_map(|package| package.complete_selections.iter().copied())
10406                .collect();
10407            let residual = source
10408                .residual_sources(&selected, &complete, &self.workbook_load_limits)
10409                .map_err(|reason| ExcelError::new(ExcelErrorKind::Value).with_message(reason))?;
10410            residual_owners.insert(
10411                sheet.clone(),
10412                residual
10413                    .1
10414                    .iter()
10415                    .map(|family| family.source_id)
10416                    .collect::<BTreeSet<_>>(),
10417            );
10418            residual_sources.insert(sheet.clone(), residual);
10419        }
10420
10421        // Reserve residual suppression before the revision-validated commit window.
10422        // Hash-set insertion and point-index removal below cannot allocate.
10423        for (sheet, points) in &selected_package_points {
10424            let package = self
10425                .staged_formulas
10426                .get_mut(sheet)
10427                .unwrap()
10428                .deferred_package
10429                .as_mut()
10430                .unwrap();
10431            let owner_count = prepared_packages
10432                .iter()
10433                .filter(|p| &p.sheet == sheet)
10434                .flat_map(|p| &p.replay_records)
10435                .filter_map(|record| record.partition_owner.or(record.family))
10436                .filter(|owner| residual_owners[sheet].contains(owner))
10437                .count();
10438            package
10439                .consumed_members
10440                .try_reserve(owner_count)
10441                .map_err(|_| {
10442                    resource(
10443                        formualizer_common::ResourceExhaustionReason::ScratchMemory,
10444                        0,
10445                        owner_count as u64 * 24,
10446                    )
10447                })?;
10448            package.suppressed.try_reserve(points.len()).map_err(|_| {
10449                resource(
10450                    formualizer_common::ResourceExhaustionReason::ScratchMemory,
10451                    0,
10452                    points.len() as u64 * 16,
10453                )
10454            })?;
10455        }
10456
10457        let estimated_commit_duration = std::time::Duration::from_nanos(
10458            (new_vertices as u64)
10459                .saturating_add(new_edges as u64)
10460                .saturating_add(prepared.len() as u64)
10461                .saturating_add(selected_package_records)
10462                .max(1)
10463                .saturating_mul(100),
10464        );
10465        if options.deadline.is_some_and(|deadline| {
10466            std::time::Instant::now()
10467                .checked_add(estimated_commit_duration)
10468                .is_none_or(|finish| finish > deadline)
10469        }) {
10470            self.observe_target_admission_failure(
10471                formualizer_common::ResourceExhaustionReason::Deadline,
10472            );
10473            return Err(resource(
10474                formualizer_common::ResourceExhaustionReason::Deadline,
10475                0,
10476                1,
10477            ));
10478        }
10479        #[cfg(test)]
10480        if let Some(hook) = self.before_target_preparation_commit_hook.take() {
10481            hook();
10482        }
10483        self.target_preparation_checkpoint(options.deadline, 0)?;
10484        #[cfg(test)]
10485        self.target_preparation_fault(
10486            crate::engine::target_preparation::TargetPreparationFault::FinalRevisionValidation,
10487        )?;
10488        let current_revisions = self.preparation_revisions();
10489        let staged_leases_match = prepared.iter().all(|formula| {
10490            self.staged_formula_index
10491                .lease_matches(&formula.sheet, formula.lease)
10492        }) && prepared_packages.iter().all(|package| {
10493            self.staged_formula_index
10494                .package_lease_matches(&package.sheet, package.lease)
10495        });
10496        let stale_reason = Self::preparation_revision_stale_reason(
10497            &assumptions,
10498            &current_revisions,
10499            &planning_requests,
10500            staged_leases_match,
10501        );
10502        if let Some(reason) = stale_reason {
10503            return Err(Self::preparation_stale(
10504                reason,
10505                "target graph preparation plan is stale",
10506            ));
10507        }
10508        #[cfg(test)]
10509        self.target_preparation_fault(
10510            crate::engine::target_preparation::TargetPreparationFault::FinalGraphValidation,
10511        )?;
10512        self.graph
10513            .validate_prepared_legacy_graph_plan(&legacy_graph)
10514            .map_err(|error| {
10515                Self::preparation_stale(
10516                    formualizer_common::PreparationStaleReason::Graph,
10517                    format!("target graph preparation plan is stale: {error}"),
10518                )
10519            })?;
10520        #[cfg(test)]
10521        self.target_preparation_fault(
10522            crate::engine::target_preparation::TargetPreparationFault::Reservation,
10523        )?;
10524        self.graph.reserve_prepared_legacy_graph_plan(&legacy_graph);
10525        self.formula_parse_diagnostics
10526            .try_reserve(pending_diagnostics.len())
10527            .map_err(|_| {
10528                resource(
10529                    formualizer_common::ResourceExhaustionReason::Admission,
10530                    pending_diagnostics.len() as u64,
10531                    pending_diagnostics.len() as u64,
10532                )
10533            })?;
10534        self.target_preparation_checkpoint(options.deadline, 0)?;
10535        #[cfg(test)]
10536        self.target_preparation_fault(
10537            crate::engine::target_preparation::TargetPreparationFault::BeforeFirstMutation,
10538        )?;
10539
10540        let commit_started = crate::instant::FzInstant::now();
10541        let committed = self
10542            .graph
10543            .apply_prevalidated_legacy_graph_plan(legacy_graph);
10544        for formula in &prepared {
10545            let removed = self
10546                .staged_formulas
10547                .get_mut(&formula.sheet)
10548                .and_then(|sheet| sheet.remove_ordinary(formula.lease.row, formula.lease.col));
10549            debug_assert!(removed.is_some());
10550            let index_removed = self.staged_formula_index.remove(
10551                &formula.sheet,
10552                formula.lease.row,
10553                formula.lease.col,
10554            );
10555            debug_assert!(index_removed);
10556        }
10557        for package in &prepared_packages {
10558            if let Some(points) = &package.selected_points {
10559                let staged = self.staged_formulas.get_mut(&package.sheet).unwrap();
10560                let source = staged.deferred_package.as_mut().unwrap();
10561                source.consumed_engine = Some(Arc::clone(&self.source_formula_token));
10562                // The revision-validated graph commit establishes this exact source
10563                // ownership proof. Later edits retain the exclusion, not old text.
10564                source
10565                    .consumed_members
10566                    .extend(package.replay_records.iter().filter_map(|record| {
10567                        record
10568                            .partition_owner
10569                            .or(record.family)
10570                            .filter(|owner| residual_owners[&package.sheet].contains(owner))
10571                            .map(|owner| {
10572                                (
10573                                    owner,
10574                                    crate::engine::SourceCoord {
10575                                        row: record.row - 1,
10576                                        col: record.col - 1,
10577                                    },
10578                                )
10579                            })
10580                    }));
10581                source.suppressed.extend(points.iter().copied());
10582                source.source_accounted = true;
10583                self.staged_formula_index
10584                    .consume_package_points(&package.sheet, points);
10585                if source.suppressed.len() >= source.source_coordinates.len() {
10586                    staged.deferred_package = None;
10587                    self.staged_formula_index.set_package(&package.sheet, None);
10588                }
10589            } else {
10590                let removed = self
10591                    .staged_formulas
10592                    .get_mut(&package.sheet)
10593                    .and_then(|staged| staged.deferred_package.take());
10594                debug_assert!(removed.is_some());
10595                self.staged_formula_index.set_package(&package.sheet, None);
10596            }
10597        }
10598        for (sheet, (families, partitions)) in residual_sources {
10599            if let Some(source) = self
10600                .staged_formulas
10601                .get_mut(&sheet)
10602                .and_then(|staged| staged.deferred_package.as_mut())
10603            {
10604                source.families = families;
10605                source.partitioned_families = partitions;
10606                source
10607                    .consumed_members
10608                    .retain(|(owner, _)| residual_owners[&sheet].contains(owner));
10609                self.staged_formula_index.update_package_family_count(
10610                    &sheet,
10611                    source.families.len() + source.partitioned_families.len(),
10612                );
10613            }
10614        }
10615        let empty_sheets = self
10616            .staged_formulas
10617            .iter()
10618            .filter_map(|(sheet, staged)| staged.is_empty().then_some(sheet.clone()))
10619            .collect::<Vec<_>>();
10620        for sheet in empty_sheets {
10621            self.staged_formulas.remove(&sheet);
10622        }
10623        if committed > 0 {
10624            self.mark_topology_edited();
10625        }
10626        self.formula_parse_diagnostics.extend(pending_diagnostics);
10627        if !prepared.is_empty() || !prepared_packages.is_empty() {
10628            let mut ingest_delta = FormulaIngestReport::with_mode(FormulaPlaneMode::Off);
10629            ingest_delta.formula_cells_seen = (prepared.len() as u64).saturating_add(
10630                prepared_packages
10631                    .iter()
10632                    .map(|package| {
10633                        (package.replay_records.len() as u64).saturating_add(
10634                            if package.selected_points.is_some() {
10635                                package.direct_cells
10636                            } else {
10637                                0
10638                            },
10639                        )
10640                    })
10641                    .sum::<u64>(),
10642            );
10643            ingest_delta.graph_formula_cells_materialized = committed as u64;
10644            ingest_delta.graph_vertices_created = new_vertices as u64;
10645            ingest_delta.graph_edges_created = new_edges as u64;
10646            for package in &prepared_packages {
10647                let source = &package.source_report;
10648                ingest_delta.source_formula_events = ingest_delta
10649                    .source_formula_events
10650                    .saturating_add(source.source_formula_events);
10651                ingest_delta.source_formula_records_spooled = ingest_delta
10652                    .source_formula_records_spooled
10653                    .saturating_add(source.source_formula_records_spooled);
10654                ingest_delta.source_spool_encoded_bytes = ingest_delta
10655                    .source_spool_encoded_bytes
10656                    .saturating_add(source.source_spool_encoded_bytes);
10657                ingest_delta.source_spool_peak_memory_bytes = ingest_delta
10658                    .source_spool_peak_memory_bytes
10659                    .max(source.source_spool_peak_memory_bytes);
10660                ingest_delta.source_spool_spilled_bytes = ingest_delta
10661                    .source_spool_spilled_bytes
10662                    .saturating_add(source.source_spool_spilled_bytes);
10663                ingest_delta.source_spool_spill_files = ingest_delta
10664                    .source_spool_spill_files
10665                    .saturating_add(source.source_spool_spill_files);
10666                ingest_delta.source_spool_replays = ingest_delta
10667                    .source_spool_replays
10668                    .saturating_add(source.source_spool_replays)
10669                    .saturating_add(package.spool_replays);
10670                ingest_delta.source_families_seen = ingest_delta
10671                    .source_families_seen
10672                    .saturating_add(source.families_seen);
10673                ingest_delta.source_family_cells_seen = ingest_delta
10674                    .source_family_cells_seen
10675                    .saturating_add(source.family_cells_seen);
10676                ingest_delta.source_family_shadow_eligible = ingest_delta
10677                    .source_family_shadow_eligible
10678                    .saturating_add(source.source_clean_families);
10679                ingest_delta.source_family_shadow_eligible_cells = ingest_delta
10680                    .source_family_shadow_eligible_cells
10681                    .saturating_add(source.source_clean_cells);
10682                ingest_delta.source_partitioned_families_seen = ingest_delta
10683                    .source_partitioned_families_seen
10684                    .saturating_add(source.source_fragmentable_families);
10685                ingest_delta.source_partition_holes = ingest_delta
10686                    .source_partition_holes
10687                    .saturating_add(source.source_hole_exclusions);
10688                ingest_delta.source_partition_ordinary_exceptions = ingest_delta
10689                    .source_partition_ordinary_exceptions
10690                    .saturating_add(source.source_ordinary_exclusions);
10691                ingest_delta.source_partition_surviving_cells = ingest_delta
10692                    .source_partition_surviving_cells
10693                    .saturating_add(source.source_fragmentable_cells);
10694                for (reason, count) in &source.fallback_reasons {
10695                    let total = ingest_delta
10696                        .fallback_reasons
10697                        .entry(reason.clone())
10698                        .or_default();
10699                    *total = total.saturating_add(*count);
10700                }
10701
10702                ingest_delta.source_family_fallback = ingest_delta
10703                    .source_family_fallback
10704                    .saturating_add(source.families_seen);
10705                ingest_delta.source_family_fallback_cells = ingest_delta
10706                    .source_family_fallback_cells
10707                    .saturating_add(source.family_cells_seen);
10708            }
10709            self.record_formula_ingest_report(ingest_delta);
10710        }
10711        let commit_window = commit_started.elapsed();
10712        let retained_cells = self
10713            .staged_formula_index
10714            .all_leases()
10715            .into_iter()
10716            .filter_map(|(sheet, lease)| {
10717                formualizer_common::RangeAddress::new(
10718                    sheet, lease.row, lease.col, lease.row, lease.col,
10719                )
10720                .ok()
10721            })
10722            .collect::<Vec<_>>();
10723        let observed_scratch_bytes = self
10724            .active_resource_ledger
10725            .as_ref()
10726            .map_or(0, |ledger| ledger.snapshot().scratch_peak)
10727            .saturating_sub(ledger_at_start.map_or(0, |snapshot| snapshot.scratch_current));
10728        let committed_spans = 0u64;
10729        let selected_source_families = prepared_packages
10730            .iter()
10731            .filter(|package| package.selected_points.is_none())
10732            .map(|package| package.lease.family_count)
10733            .sum::<usize>()
10734            + prepared_packages
10735                .iter()
10736                .filter(|package| package.selected_points.is_some())
10737                .flat_map(|package| package.replay_records.iter())
10738                .filter_map(|record| record.partition_owner.or(record.family))
10739                .chain(
10740                    prepared_packages
10741                        .iter()
10742                        .flat_map(|package| package.complete_selections.iter().copied()),
10743                )
10744                .collect::<BTreeSet<_>>()
10745                .len();
10746        let selected_staged_cells = prepared.len().saturating_add(
10747            prepared_packages
10748                .iter()
10749                .map(|package| {
10750                    package
10751                        .selected_points
10752                        .as_ref()
10753                        .map_or(package.replay_records.len(), BTreeSet::len)
10754                })
10755                .sum::<usize>(),
10756        );
10757        let actual_commit_work = (new_vertices as u64)
10758            .saturating_add(new_edges as u64)
10759            .saturating_add(committed as u64)
10760            .saturating_add(committed_spans)
10761            .saturating_add(prepared.len() as u64)
10762            .saturating_add(
10763                prepared_packages
10764                    .iter()
10765                    .map(|package| {
10766                        package
10767                            .selected_points
10768                            .as_ref()
10769                            .map_or(1, |points| points.len() as u64)
10770                    })
10771                    .sum::<u64>(),
10772            );
10773        let report = PreparedTargetGraphReport {
10774            request_id: request_id.unwrap_or_default(),
10775            requested_targets: targets.len(),
10776            normalized_regions: visited_regions.len(),
10777            normalized_target_list: normalized,
10778            selected_staged_cells,
10779            selected_source_families,
10780            retained_staged_cells: self.staged_formula_count(),
10781            selected_cells,
10782            retained_cells,
10783            widened_scope: scope,
10784            widening_reasons: reasons,
10785            revisions: assumptions,
10786            commit_window,
10787            estimated_scratch_bytes: scratch_bytes,
10788            observed_scratch_bytes,
10789            estimated_commit_work: (new_vertices as u64)
10790                .saturating_add(new_edges as u64)
10791                .saturating_add(prepared.len() as u64)
10792                .saturating_add(selected_package_records),
10793            actual_commit_work,
10794            outcome: PreparationOutcome::Prepared,
10795        };
10796        self.observe_target_preparation_report(&report);
10797        Ok(report)
10798    }
10799
10800    /// Build graph for all staged formulas.
10801    pub fn build_graph_all(&mut self) -> Result<(), formualizer_parse::ExcelError> {
10802        self.ensure_not_poisoned()?;
10803        self.observe_evaluation_resource_request(EvaluationRequestKind::Full, |engine| {
10804            engine.build_graph_all_unobserved()
10805        })
10806    }
10807
10808    fn build_graph_all_unobserved(&mut self) -> Result<(), formualizer_parse::ExcelError> {
10809        let selected = self.staged_formula_count();
10810        let started = crate::instant::FzInstant::now();
10811        self.resource_checkpoint(selected as u64)?;
10812        let scratch_bytes = (selected as u64).saturating_mul(256);
10813        let result = self.with_request_scratch(scratch_bytes, |engine| {
10814            let index_snapshot = engine.staged_formula_index.clone();
10815            let collected = std::mem::take(&mut engine.staged_formulas)
10816                .into_iter()
10817                .collect();
10818            engine.staged_formula_index.clear_all();
10819            engine.build_graph_from_staged_batches(collected, false, index_snapshot)
10820        });
10821        self.observe_staged_preparation(selected, self.staged_formula_count(), started.elapsed());
10822        result
10823    }
10824
10825    /// Build graph for specific sheets (consuming only those staged entries).
10826    pub fn build_graph_for_sheets<'a, I: IntoIterator<Item = &'a str>>(
10827        &mut self,
10828        sheets: I,
10829    ) -> Result<(), formualizer_parse::ExcelError> {
10830        self.ensure_not_poisoned()?;
10831        let mut sheets = sheets.into_iter();
10832        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, move |engine| {
10833            let name_scratch = (sheets.size_hint().0 as u64).saturating_mul(64);
10834            engine.with_request_scratch(name_scratch, |engine| {
10835                // Allocation failure follows the baseline process-fatal policy; it is not a
10836                // recoverable resource error or a new staged-preparation route.
10837                let names = sheets.by_ref().map(str::to_string).collect::<Vec<_>>();
10838                engine.charge_bounded_work(names.len() as u64)?;
10839                engine.build_graph_for_sheet_names_unobserved(names)
10840            })
10841        })
10842    }
10843
10844    fn build_graph_for_sheet_names_unobserved(
10845        &mut self,
10846        sheets: Vec<String>,
10847    ) -> Result<(), formualizer_parse::ExcelError> {
10848        let started = crate::instant::FzInstant::now();
10849        let selected = sheets
10850            .iter()
10851            .filter_map(|sheet| self.staged_formulas.get(sheet))
10852            .map(StagedSheet::len)
10853            .sum::<usize>();
10854        self.resource_checkpoint(selected as u64)?;
10855        let scratch_bytes = (selected as u64).saturating_mul(256);
10856        self.reserve_request_scratch(scratch_bytes)?;
10857        let index_snapshot = self.staged_formula_index.clone();
10858        let mut collected = Vec::new();
10859        for sheet in sheets {
10860            if let Some(staged) = self.staged_formulas.remove(&sheet) {
10861                self.index_removed_staged_sheet(&sheet, &staged);
10862                collected.push((sheet, staged));
10863            }
10864        }
10865        let result = self.build_graph_from_staged_batches(collected, true, index_snapshot);
10866        self.release_request_scratch(scratch_bytes);
10867        self.observe_staged_preparation(selected, self.staged_formula_count(), started.elapsed());
10868        result
10869    }
10870
10871    fn build_graph_from_staged_batches(
10872        &mut self,
10873        collected: StagedFormulaBatches,
10874        share_parse_cache_across_sheets: bool,
10875        staged_index_snapshot: StagedFormulaIndex,
10876    ) -> Result<(), formualizer_parse::ExcelError> {
10877        if collected.is_empty() {
10878            return Ok(());
10879        }
10880        for (sheet, _) in &collected {
10881            let _ = self.add_sheet(sheet);
10882        }
10883
10884        let diagnostics_len = self.formula_parse_diagnostics.len();
10885        let mut collected = collected;
10886        let prepared = match self
10887            .prepare_staged_formula_batches(&mut collected, share_parse_cache_across_sheets)
10888        {
10889            Ok(prepared) => prepared,
10890            Err(error) => {
10891                self.formula_parse_diagnostics.truncate(diagnostics_len);
10892                for (sheet, staged) in collected {
10893                    self.restore_staged_sheet(sheet, staged);
10894                }
10895                self.staged_formula_index = staged_index_snapshot;
10896                return Err(error);
10897            }
10898        };
10899        let (ordinary, compressed, direct, may_fail) = prepared;
10900
10901        // A first build (no formula in the graph yet) goes through the eager
10902        // first load's machinery: the builder pre-allocates each column's
10903        // targets as one id run, installs family members as virtual runs and
10904        // builds the authority once at the end (decision 26).
10905        // The first load's builder plans and applies one chunk at a time, so
10906        // a planning error would leave earlier chunks in the graph; the
10907        // incremental path plans everything first. Check every distinct
10908        // formula first, in the incremental path's order (it reports the
10909        // same first error and leaves the graph untouched).
10910        let first_build = if self.deferred_build_can_be_first_load() {
10911            if let Err(error) = self.check_staged_formula_plans(
10912                ordinary
10913                    .iter()
10914                    .chain(compressed.iter().map(|(batch, _)| batch)),
10915                &may_fail,
10916            ) {
10917                self.formula_parse_diagnostics.truncate(diagnostics_len);
10918                for (sheet, staged) in collected {
10919                    self.restore_staged_sheet(sheet, staged);
10920                }
10921                self.staged_formula_index = staged_index_snapshot;
10922                return Err(error);
10923            }
10924            self.deferred_build_as_first_load()
10925        } else {
10926            None
10927        };
10928        let built_sheets: Vec<String> = if first_build.is_some() {
10929            collected.iter().map(|(sheet, _)| sheet.clone()).collect()
10930        } else {
10931            Vec::new()
10932        };
10933
10934        // Keep the original source/spool alive through every fallible ingestion route.
10935        // Graph admission may have committed a prefix; replay replaces those placements
10936        // rather than treating their cached values as authoritative source.
10937        let result = (|| {
10938            if !ordinary.is_empty() {
10939                self.ingest_formula_batches(ordinary)?;
10940            }
10941            if !compressed.is_empty() {
10942                self.ingest_compressed_formula_source_batches(compressed)?;
10943            }
10944            if !direct.is_empty() {
10945                self.finish_compressed_formula_sources(direct)?;
10946            }
10947            Ok(())
10948        })();
10949        if let Some(saved) = first_build {
10950            self.leave_deferred_first_load(saved, &built_sheets);
10951        }
10952        if let Err(error) = result {
10953            self.formula_parse_diagnostics.truncate(diagnostics_len);
10954            for (sheet, staged) in collected {
10955                self.restore_staged_sheet(sheet, staged);
10956            }
10957            self.staged_formula_index = staged_index_snapshot;
10958            return Err(error);
10959        }
10960        self.dedup_formula_parse_diagnostics_since(diagnostics_len);
10961        Ok(())
10962    }
10963
10964    /// Enter the first-load ingest mode for a deferred build when the graph
10965    /// holds no formula yet and no load is in progress: the settings the
10966    /// calamine loader uses for its eager first load (lazy sheet index, no
10967    /// small-range expansion, first-load fast path). Returns the settings
10968    /// to restore, or `None` when the build takes the incremental path.
10969    fn deferred_build_can_be_first_load(&self) -> bool {
10970        !self.graph.first_load_assume_new()
10971            && !self.graph_admission_enabled()
10972            && self.graph.formula_vertex_count() == 0
10973    }
10974
10975    /// Plan each distinct formula of `batches` that may fail planning once
10976    /// (a member is planned through its template, staged before it) and
10977    /// drop the plans: the first planning error, in the order the
10978    /// incremental ingest meets it.
10979    fn check_staged_formula_plans<'b>(
10980        &mut self,
10981        batches: impl Iterator<Item = &'b FormulaIngestBatch>,
10982        may_fail: &FxHashSet<crate::engine::arena::AstNodeId>,
10983    ) -> Result<(), ExcelError> {
10984        if may_fail.is_empty() {
10985            return Ok(());
10986        }
10987        let mut seen: FxHashSet<(SheetId, crate::engine::arena::AstNodeId)> = FxHashSet::default();
10988        for batch in batches {
10989            let sheet_id = self.graph.sheet_id(&batch.sheet_name).ok_or_else(|| {
10990                ExcelError::new(ExcelErrorKind::Ref)
10991                    .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
10992            })?;
10993            let mut pipeline = self.ingest_pipeline();
10994            for record in &batch.formulas {
10995                if record.member_anchor.is_some()
10996                    || !may_fail.contains(&record.ast_id)
10997                    || !seen.insert((sheet_id, record.ast_id))
10998                {
10999                    continue;
11000                }
11001                let placement = CellRef::new(
11002                    sheet_id,
11003                    Coord::from_excel(record.row, record.col, true, true),
11004                );
11005                pipeline.ingest_formula(
11006                    FormulaAstInput::RawArena(record.ast_id),
11007                    placement,
11008                    None,
11009                )?;
11010            }
11011        }
11012        Ok(())
11013    }
11014
11015    fn deferred_build_as_first_load(&mut self) -> Option<(crate::engine::SheetIndexMode, usize)> {
11016        if !self.deferred_build_can_be_first_load() {
11017            return None;
11018        }
11019        let saved = (
11020            self.graph.get_config().sheet_index_mode,
11021            self.config.range_expansion_limit,
11022        );
11023        self.graph
11024            .set_sheet_index_mode(crate::engine::SheetIndexMode::Lazy);
11025        self.config.range_expansion_limit = 0;
11026        self.graph.set_first_load_assume_new(true);
11027        self.graph.reset_ensure_touched();
11028        Some(saved)
11029    }
11030
11031    /// Leave the first-load mode of [`Self::deferred_build_as_first_load`]:
11032    /// the authority is built once here, as at the end of an eager load.
11033    fn leave_deferred_first_load(
11034        &mut self,
11035        (index_mode, range_limit): (crate::engine::SheetIndexMode, usize),
11036        sheets: &[String],
11037    ) {
11038        self.graph.set_first_load_assume_new(false);
11039        self.graph.reset_ensure_touched();
11040        self.graph.set_sheet_index_mode(index_mode);
11041        self.config.range_expansion_limit = range_limit;
11042        for sheet in sheets {
11043            self.graph.finalize_sheet_index(sheet);
11044        }
11045    }
11046
11047    fn prepare_staged_formula_batches(
11048        &mut self,
11049        collected: &mut StagedFormulaBatches,
11050        share_parse_cache_across_sheets: bool,
11051    ) -> Result<PreparedStagedFormulaBatches, formualizer_parse::ExcelError> {
11052        let mut ordinary = Vec::new();
11053        let mut compressed = Vec::new();
11054        let mut direct = Vec::new();
11055        let mut may_fail = FxHashSet::default();
11056        let mut cache: rustc_hash::FxHashMap<String, Option<crate::engine::arena::AstNodeId>> =
11057            rustc_hash::FxHashMap::default();
11058        cache.reserve(4096);
11059
11060        for (sheet, staged) in collected {
11061            if !share_parse_cache_across_sheets {
11062                cache.clear();
11063            }
11064            // Load-time family grouping, as the eager first load does:
11065            // relative copies of the formula above (or to the left) become
11066            // members of its family and are never interned.
11067            let mut grouper = crate::engine::FormulaFamilyGrouper::new();
11068            // The staged texts, then the deferred package's replayed ones,
11069            // are walked once (no combined copy: a replay can be the whole
11070            // sheet).
11071            let mut replayed_records = Vec::new();
11072            let deferred_source = None;
11073            let mut deferred_fallback = None;
11074            if let Some(package) = staged.deferred_package.as_mut() {
11075                if package.sheet_name != *sheet {
11076                    return Err(ExcelError::new(ExcelErrorKind::Value)
11077                        .with_message("deferred formula package sheet mismatch"));
11078                }
11079                let eligible: Vec<_> = package
11080                    .families
11081                    .iter()
11082                    .filter(|family| !package.invalidated.contains(&family.source_id))
11083                    .cloned()
11084                    .collect();
11085                let eligible_partitions: Vec<_> = package
11086                    .partitioned_families
11087                    .iter()
11088                    .filter(|family| !package.invalidated.contains(&family.source_id))
11089                    .cloned()
11090                    .collect();
11091
11092                let mut replay_disposition = crate::engine::FormulaReplayDisposition::default();
11093                for partition in &eligible_partitions {
11094                    replay_disposition
11095                        .register_partition(partition, false)
11096                        .map_err(|reason| {
11097                            ExcelError::new(ExcelErrorKind::Value).with_message(reason)
11098                        })?;
11099                }
11100                replay_disposition
11101                    .extend_suppressed_excel_coords(package.suppressed.iter().copied());
11102                let replayed = package
11103                    .replay
11104                    .lock()
11105                    .map_err(|_| {
11106                        ExcelError::new(ExcelErrorKind::Value)
11107                            .with_message("deferred formula spool lock poisoned")
11108                    })?
11109                    .replay(&replay_disposition)
11110                    .map_err(|message| {
11111                        ExcelError::new(ExcelErrorKind::Value).with_message(message)
11112                    })?;
11113                replayed_records = replayed;
11114                let mut report = package.accounting_report();
11115                report.source_spool_replays = report.source_spool_replays.saturating_add(1);
11116                deferred_fallback = Some((report, package.families.clone(), eligible_partitions));
11117            }
11118
11119            let n_entries = staged.entries.len() + replayed_records.len();
11120            let entries = staged
11121                .entries
11122                .iter()
11123                .cloned()
11124                .map(|(row, col, text)| (row, col, text, None))
11125                .chain(replayed_records.into_iter().map(|record| {
11126                    (
11127                        record.row,
11128                        record.col,
11129                        record.text,
11130                        Some((record.source_order, record.family, record.partition_owner)),
11131                    )
11132                }));
11133            let mut formulas = Vec::with_capacity(n_entries);
11134            let staged_order_base = u64::MAX.saturating_sub(n_entries as u64);
11135            for (entry_index, (row, col, txt, source_proof)) in entries.enumerate() {
11136                let key = if txt.starts_with('=') {
11137                    txt
11138                } else {
11139                    format!("={txt}")
11140                };
11141                grouper.note_formula();
11142                // A shared-formula family's records name their family.
11143                let shared_key = source_proof
11144                    .and_then(|(_, family, _)| family)
11145                    .map(|family| u64::from(family.source_index));
11146                let staged_record = if let Some(cached) = cache.get(&key) {
11147                    grouper.note_parse_cache_hit();
11148                    cached.map(|ast_id| {
11149                        self.note_staged_formula(&mut grouper, row, col, ast_id);
11150                        FormulaIngestRecord::new(row, col, ast_id, Some(Arc::<str>::from(key)))
11151                    })
11152                } else if let Some(record) =
11153                    self.stage_relocated_text(&mut grouper, row, col, &key, shared_key)
11154                {
11155                    Some(record)
11156                } else {
11157                    grouper.note_parse(key.len());
11158                    let parsed = formualizer_parse::parser::parse(&key);
11159                    let parsed_text = parsed.is_ok();
11160                    let parsed = match parsed {
11161                        Ok(parsed) => Some(parsed),
11162                        Err(error) => self.handle_formula_parse_error(
11163                            sheet,
11164                            row,
11165                            col,
11166                            &key,
11167                            error.to_string(),
11168                        )?,
11169                    };
11170                    match parsed {
11171                        Some(ast) => {
11172                            let record = self.stage_formula_ast(&mut grouper, row, col, &ast, None);
11173                            if parsed_text {
11174                                self.note_parsed_text(
11175                                    &mut grouper,
11176                                    row,
11177                                    col,
11178                                    &key,
11179                                    &ast,
11180                                    &record,
11181                                    shared_key,
11182                                );
11183                            }
11184                            if !record.is_family_member() && self.formula_may_fail_planning(&ast) {
11185                                may_fail.insert(record.ast_id);
11186                            }
11187                            // A member's text is not worth caching: relative
11188                            // copies do not repeat their text.
11189                            if record.is_family_member() {
11190                                Some(record)
11191                            } else {
11192                                let ast_id = record.ast_id;
11193                                cache.insert(key.clone(), Some(ast_id));
11194                                Some(FormulaIngestRecord::new(
11195                                    row,
11196                                    col,
11197                                    ast_id,
11198                                    Some(Arc::<str>::from(key)),
11199                                ))
11200                            }
11201                        }
11202                        None => {
11203                            cache.insert(key, None);
11204                            None
11205                        }
11206                    }
11207                };
11208
11209                if let Some(mut formula) = staged_record {
11210                    if let Some((order, family, owner)) = source_proof {
11211                        formula = formula.with_source_proof(order, family, owner);
11212                    } else if deferred_source.is_some() {
11213                        formula = formula.with_source_proof(
11214                            crate::engine::SourceFormulaOrder::new(
11215                                staged_order_base.saturating_add(entry_index as u64),
11216                            ),
11217                            None,
11218                            None,
11219                        );
11220                    }
11221                    formulas.push(formula);
11222                }
11223            }
11224
11225            self.finish_family_grouper(&mut grouper);
11226            let batch = FormulaIngestBatch::new(sheet.clone(), formulas);
11227            if let Some((report, preparation)) = deferred_source {
11228                direct.push((batch, report, preparation));
11229            } else if let Some((report, families, partitions)) = deferred_fallback {
11230                let source_batch = crate::engine::FormulaCompressedSourceBatch::with_proposals(
11231                    batch.sheet_name.clone(),
11232                    report,
11233                    families,
11234                    partitions,
11235                );
11236                compressed.push((batch, source_batch));
11237            } else if !batch.is_empty() {
11238                ordinary.push(batch);
11239            }
11240        }
11241        if std::env::var("FZ_DEBUG_LOAD")
11242            .ok()
11243            .is_some_and(|v| v != "0")
11244        {
11245            eprintln!(
11246                "[fz][families] deferred build: {}",
11247                self.source_family_counters.debug_line()
11248            );
11249        }
11250        Ok((ordinary, compressed, direct, may_fail))
11251    }
11252
11253    /// Whether planning `ast` can fail: a reference to a sheet that does not
11254    /// exist, an external, 3-D or table reference, or a reversed range.
11255    /// Unqualified cell and range references, names, literals and calls
11256    /// always plan.
11257    fn formula_may_fail_planning(&self, ast: &formualizer_parse::parser::ASTNode) -> bool {
11258        use formualizer_parse::parser::{ASTNodeType, ReferenceType};
11259        let sheet_missing = |sheet: &Option<String>| {
11260            sheet
11261                .as_deref()
11262                .is_some_and(|s| self.graph.sheet_id(s).is_none())
11263        };
11264        match &ast.node_type {
11265            ASTNodeType::Literal(_) | ASTNodeType::Omitted => false,
11266            ASTNodeType::Reference { reference, .. } => match reference {
11267                ReferenceType::Cell { sheet, .. } => sheet_missing(sheet),
11268                ReferenceType::Range {
11269                    sheet,
11270                    start_row,
11271                    start_col,
11272                    end_row,
11273                    end_col,
11274                    ..
11275                } => {
11276                    sheet_missing(sheet)
11277                        || matches!((start_row, end_row), (Some(a), Some(b)) if a > b)
11278                        || matches!((start_col, end_col), (Some(a), Some(b)) if a > b)
11279                }
11280                ReferenceType::NamedRange(_) => false,
11281                _ => true,
11282            },
11283            ASTNodeType::UnaryOp { expr, .. } => self.formula_may_fail_planning(expr),
11284            ASTNodeType::BinaryOp { left, right, .. } => {
11285                self.formula_may_fail_planning(left) || self.formula_may_fail_planning(right)
11286            }
11287            ASTNodeType::Function { args, .. } => {
11288                args.iter().any(|a| self.formula_may_fail_planning(a))
11289            }
11290            ASTNodeType::Call { callee, args } => {
11291                self.formula_may_fail_planning(callee)
11292                    || args.iter().any(|a| self.formula_may_fail_planning(a))
11293            }
11294            ASTNodeType::Array(rows) => rows
11295                .iter()
11296                .flatten()
11297                .any(|a| self.formula_may_fail_planning(a)),
11298        }
11299    }
11300
11301    /// Begin bulk Arrow ingest for base values (Phase A)
11302    ///
11303    /// # Panics
11304    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
11305    pub fn begin_bulk_ingest_arrow(
11306        &mut self,
11307    ) -> crate::engine::arrow_ingest::ArrowBulkIngestBuilder<'_, R> {
11308        self.assert_not_poisoned();
11309        crate::engine::arrow_ingest::ArrowBulkIngestBuilder::new(self)
11310    }
11311
11312    /// Begin bulk updates to Arrow store (Phase C)
11313    ///
11314    /// # Panics
11315    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
11316    pub fn begin_bulk_update_arrow(
11317        &mut self,
11318    ) -> crate::engine::arrow_ingest::ArrowBulkUpdateBuilder<'_, R> {
11319        self.assert_not_poisoned();
11320        crate::engine::arrow_ingest::ArrowBulkUpdateBuilder::new(self)
11321    }
11322
11323    fn ensure_known_sheet_id(&self, sheet: &str) -> Result<SheetId, crate::engine::EditorError> {
11324        self.graph.sheet_id(sheet).ok_or(
11325            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
11326                name: sheet.to_string(),
11327                reason: "Unknown sheet".to_string(),
11328            },
11329        )
11330    }
11331
11332    fn normalize_row_1based(row_1based: u32) -> Result<u32, crate::engine::EditorError> {
11333        if row_1based == 0 {
11334            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
11335        }
11336        Ok(row_1based - 1)
11337    }
11338
11339    fn normalize_row_range_1based(
11340        start_row_1based: u32,
11341        end_row_1based: u32,
11342    ) -> Result<(u32, u32), crate::engine::EditorError> {
11343        if start_row_1based == 0 || end_row_1based == 0 {
11344            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
11345        }
11346        if start_row_1based > end_row_1based {
11347            return Err(crate::engine::EditorError::TransactionFailed {
11348                reason: "Row range start is greater than end".to_string(),
11349            });
11350        }
11351        Ok((start_row_1based - 1, end_row_1based - 1))
11352    }
11353
11354    fn invalidate_row_visibility_mask_cache(&self) {
11355        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
11356            cache.clear();
11357        }
11358    }
11359
11360    /// Hidden rows changed: drop the cached masks and dirty the formulas
11361    /// that read row visibility (SUBTOTAL/AGGREGATE).
11362    fn row_visibility_changed(&mut self) {
11363        self.invalidate_row_visibility_mask_cache();
11364        self.graph.mark_row_visibility_readers_dirty();
11365    }
11366
11367    fn set_row_hidden_by_sheet_id(
11368        &mut self,
11369        sheet_id: SheetId,
11370        row0: u32,
11371        hidden: bool,
11372        source: RowVisibilitySource,
11373    ) -> bool {
11374        let changed = {
11375            let state = self.row_visibility.entry(sheet_id).or_default();
11376            state.set_row_hidden(row0, hidden, source)
11377        };
11378
11379        let remove_entry = self
11380            .row_visibility
11381            .get(&sheet_id)
11382            .map(|state| state.is_empty())
11383            .unwrap_or(false);
11384        if remove_entry {
11385            self.row_visibility.remove(&sheet_id);
11386        }
11387
11388        if changed {
11389            self.row_visibility_changed();
11390        }
11391
11392        changed
11393    }
11394
11395    fn set_rows_hidden_by_sheet_id(
11396        &mut self,
11397        sheet_id: SheetId,
11398        start_row0: u32,
11399        end_row0: u32,
11400        hidden: bool,
11401        source: RowVisibilitySource,
11402    ) -> bool {
11403        let changed = {
11404            let state = self.row_visibility.entry(sheet_id).or_default();
11405            state.set_rows_hidden(start_row0, end_row0, hidden, source)
11406        };
11407
11408        let remove_entry = self
11409            .row_visibility
11410            .get(&sheet_id)
11411            .map(|state| state.is_empty())
11412            .unwrap_or(false);
11413        if remove_entry {
11414            self.row_visibility.remove(&sheet_id);
11415        }
11416
11417        if changed {
11418            self.row_visibility_changed();
11419        }
11420
11421        changed
11422    }
11423
11424    fn shift_row_visibility_insert(&mut self, sheet_id: SheetId, before0: u32, count: u32) {
11425        if count == 0 {
11426            return;
11427        }
11428        let mut changed = false;
11429        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
11430            changed = state.insert_rows(before0, count);
11431            state.is_empty()
11432        } else {
11433            false
11434        };
11435        if remove_entry {
11436            self.row_visibility.remove(&sheet_id);
11437        }
11438        if changed {
11439            self.row_visibility_changed();
11440        }
11441    }
11442
11443    fn shift_row_visibility_delete(&mut self, sheet_id: SheetId, start0: u32, count: u32) {
11444        if count == 0 {
11445            return;
11446        }
11447        let mut changed = false;
11448        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
11449            changed = state.delete_rows(start0, count);
11450            state.is_empty()
11451        } else {
11452            false
11453        };
11454        if remove_entry {
11455            self.row_visibility.remove(&sheet_id);
11456        }
11457        if changed {
11458            self.row_visibility_changed();
11459        }
11460    }
11461
11462    fn apply_inverse_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
11463        if let crate::engine::ChangeEvent::SetRowVisibility {
11464            sheet_id,
11465            row0,
11466            source,
11467            old_hidden,
11468            ..
11469        } = event
11470        {
11471            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *old_hidden, *source);
11472        }
11473    }
11474
11475    fn apply_forward_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
11476        if let crate::engine::ChangeEvent::SetRowVisibility {
11477            sheet_id,
11478            row0,
11479            source,
11480            new_hidden,
11481            ..
11482        } = event
11483        {
11484            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *new_hidden, *source);
11485        }
11486    }
11487
11488    fn apply_inverse_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
11489        for event in events.iter().rev() {
11490            self.apply_inverse_row_visibility_event(event);
11491        }
11492    }
11493
11494    fn apply_forward_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
11495        for event in events {
11496            self.apply_forward_row_visibility_event(event);
11497        }
11498    }
11499
11500    fn apply_inverse_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
11501        if let crate::engine::ChangeEvent::StagedFormulaCellChanged {
11502            sheet,
11503            row,
11504            col,
11505            old,
11506            ..
11507        } = event
11508        {
11509            self.apply_staged_formula_cell(sheet, *row, *col, old.as_deref());
11510        }
11511    }
11512
11513    fn apply_forward_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
11514        if let crate::engine::ChangeEvent::StagedFormulaCellChanged {
11515            sheet,
11516            row,
11517            col,
11518            new,
11519            ..
11520        } = event
11521        {
11522            self.apply_staged_formula_cell(sheet, *row, *col, new.as_deref());
11523        }
11524    }
11525
11526    /// Set a single cell's staged formula text to `target` (clearing it when
11527    /// `None`). Used by undo/redo replay of per-cell staged-formula deltas.
11528    fn apply_staged_formula_cell(&mut self, sheet: &str, row: u32, col: u32, target: Option<&str>) {
11529        match target {
11530            Some(text) => self.stage_formula_text(sheet, row, col, text.to_string()),
11531            None => {
11532                self.clear_staged_formula_text(sheet, row, col);
11533            }
11534        }
11535    }
11536
11537    pub fn set_row_hidden(
11538        &mut self,
11539        sheet: &str,
11540        row_1based: u32,
11541        hidden: bool,
11542        source: RowVisibilitySource,
11543    ) -> Result<(), crate::engine::EditorError> {
11544        self.ensure_not_poisoned()?;
11545        self.observe_function_semantic_epoch()
11546            .map_err(crate::engine::EditorError::Excel)?;
11547        self.observe_function_semantic_epoch()
11548            .map_err(crate::engine::EditorError::Excel)?;
11549        let sheet_id = self.ensure_known_sheet_id(sheet)?;
11550        let row0 = Self::normalize_row_1based(row_1based)?;
11551        if self.set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source) {
11552            self.record_structural_change(StructuralScope::Region(Region::whole_row(
11553                sheet_id, row0,
11554            )));
11555            self.mark_data_edited();
11556        }
11557        Ok(())
11558    }
11559
11560    pub fn set_rows_hidden(
11561        &mut self,
11562        sheet: &str,
11563        start_row_1based: u32,
11564        end_row_1based: u32,
11565        hidden: bool,
11566        source: RowVisibilitySource,
11567    ) -> Result<(), crate::engine::EditorError> {
11568        self.ensure_not_poisoned()?;
11569        let sheet_id = self.ensure_known_sheet_id(sheet)?;
11570        let (start_row0, end_row0) =
11571            Self::normalize_row_range_1based(start_row_1based, end_row_1based)?;
11572        if self.set_rows_hidden_by_sheet_id(sheet_id, start_row0, end_row0, hidden, source) {
11573            if start_row0 == end_row0 {
11574                self.record_structural_change(StructuralScope::Region(Region::whole_row(
11575                    sheet_id, start_row0,
11576                )));
11577            } else {
11578                self.record_structural_change(StructuralScope::Sheet(sheet_id));
11579            }
11580            self.mark_data_edited();
11581        }
11582        Ok(())
11583    }
11584
11585    pub fn is_row_hidden(
11586        &self,
11587        sheet: &str,
11588        row_1based: u32,
11589        source: Option<RowVisibilitySource>,
11590    ) -> Option<bool> {
11591        let sheet_id = self.graph.sheet_id(sheet)?;
11592        let row0 = row_1based.checked_sub(1)?;
11593        Some(
11594            self.row_visibility
11595                .get(&sheet_id)
11596                .map(|state| state.is_row_hidden(row0, source))
11597                .unwrap_or(false),
11598        )
11599    }
11600
11601    pub fn row_visibility_version(&self, sheet: &str) -> Option<u64> {
11602        let sheet_id = self.graph.sheet_id(sheet)?;
11603        Some(
11604            self.row_visibility
11605                .get(&sheet_id)
11606                .map(|state| state.version())
11607                .unwrap_or(0),
11608        )
11609    }
11610
11611    /// Cells of a sheet-backed `view` whose formula (own or a family
11612    /// template) calls SUBTOTAL, or AGGREGATE when `include_aggregate`, as
11613    /// sorted, disjoint `(col, first_row, last_row)` offset intervals within
11614    /// the view. Each formula's call bits are precomputed in the AST arena;
11615    /// a filled-down family is checked once per run, not once per cell.
11616    fn nested_subtotal_cells_for_view(
11617        &self,
11618        view: &RangeView<'_>,
11619        include_aggregate: bool,
11620    ) -> Option<Vec<(usize, usize, usize)>> {
11621        if !view.is_sheet_backed() {
11622            return None;
11623        }
11624        let (rows, cols) = view.dims();
11625        if rows == 0 || cols == 0 {
11626            return Some(Vec::new());
11627        }
11628        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
11629        let (r0, c0) = (view.start_row(), view.start_col());
11630        let mask = if include_aggregate {
11631            crate::engine::arena::SUBTOTAL_CALL | crate::engine::arena::AGGREGATE_CALL
11632        } else {
11633            crate::engine::arena::SUBTOTAL_CALL
11634        };
11635        let ds = self.graph.data_store();
11636        let mut found: Vec<(usize, usize, usize)> = self
11637            .graph
11638            .formula_intervals_in_region(
11639                sheet_id,
11640                r0 as u32,
11641                (r0 + rows - 1) as u32,
11642                c0 as u32,
11643                (c0 + cols - 1) as u32,
11644                |template| ds.ast_subtotal_calls(template) & mask != 0,
11645            )
11646            .into_iter()
11647            .map(|(col, lo, hi)| (col as usize - c0, lo as usize - r0, hi as usize - r0))
11648            .collect();
11649        found.sort_unstable();
11650        // Merge touching or overlapping intervals of a column so a binary
11651        // search finds the one covering a cell.
11652        let mut out: Vec<(usize, usize, usize)> = Vec::with_capacity(found.len());
11653        for (col, lo, hi) in found {
11654            match out.last_mut() {
11655                Some(last) if last.0 == col && lo <= last.2 + 1 => last.2 = last.2.max(hi),
11656                _ => out.push((col, lo, hi)),
11657            }
11658        }
11659        Some(out)
11660    }
11661
11662    fn build_row_visibility_mask_for_view(
11663        &self,
11664        view: &RangeView<'_>,
11665        mode: VisibilityMaskMode,
11666    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
11667        let sheet_rows = view.sheet().nrows as usize;
11668        if sheet_rows == 0 || view.start_row() >= sheet_rows {
11669            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
11670        }
11671
11672        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
11673        let start_row0 = view.start_row() as u32;
11674        let end_row0 = view.end_row().min(sheet_rows.saturating_sub(1)) as u32;
11675        let version = self
11676            .row_visibility
11677            .get(&sheet_id)
11678            .map(|state| state.version())
11679            .unwrap_or(0);
11680        let key = VisibilityMaskCacheKey {
11681            sheet_id,
11682            start_row0,
11683            end_row0,
11684            mode,
11685            version,
11686        };
11687
11688        if let Ok(cache) = self.row_visibility_mask_cache.read()
11689            && let Some(mask) = cache.get(&key)
11690        {
11691            #[cfg(test)]
11692            visibility_mask_test_hooks::inc_hit();
11693            return Some(mask.clone());
11694        }
11695
11696        #[cfg(test)]
11697        visibility_mask_test_hooks::inc_miss();
11698
11699        let state = self.row_visibility.get(&sheet_id);
11700        let mut out = Vec::with_capacity((end_row0 - start_row0 + 1) as usize);
11701        for row0 in start_row0..=end_row0 {
11702            let manual_hidden = state
11703                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Manual)))
11704                .unwrap_or(false);
11705            let filter_hidden = state
11706                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Filter)))
11707                .unwrap_or(false);
11708
11709            let include = match mode {
11710                VisibilityMaskMode::IncludeAll => true,
11711                VisibilityMaskMode::ExcludeManualHidden => !manual_hidden,
11712                VisibilityMaskMode::ExcludeFilterHidden => !filter_hidden,
11713                VisibilityMaskMode::ExcludeManualOrFilterHidden => {
11714                    !(manual_hidden || filter_hidden)
11715                }
11716            };
11717            out.push(include);
11718        }
11719
11720        let mask = std::sync::Arc::new(arrow_array::BooleanArray::from(out));
11721        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
11722            const MAX_CACHE_ENTRIES: usize = 4096;
11723            if cache.len() >= MAX_CACHE_ENTRIES {
11724                cache.clear();
11725                #[cfg(test)]
11726                visibility_mask_test_hooks::inc_eviction();
11727            }
11728            cache.insert(key, mask.clone());
11729        }
11730
11731        Some(mask)
11732    }
11733
11734    fn observe_function_semantic_epoch(&mut self) -> Result<bool, ExcelError> {
11735        let changes =
11736            crate::function_registry::semantic_changes_since(self.function_semantic_epoch_seen);
11737        let global_changed = changes.epoch != self.function_semantic_epoch_seen;
11738        let provider_revision = self.resolver.planning_semantic_revision();
11739        let provider_changed = provider_revision != self.function_provider_revision_seen;
11740        if !global_changed && !provider_changed {
11741            return Ok(false);
11742        }
11743
11744        let changed = !changes.keys.is_empty();
11745        if global_changed && changed || provider_changed {
11746            self.cached_static_schedule = None;
11747            self.recent_schedules.clear();
11748            self.base_schedule = None;
11749        }
11750        self.function_semantic_epoch_seen = changes.epoch;
11751        self.function_provider_revision_seen = provider_revision;
11752        Ok(false)
11753    }
11754
11755    pub(crate) fn ast_uses_changed_function(
11756        ast: &ASTNode,
11757        changed: &BTreeSet<(String, String)>,
11758    ) -> bool {
11759        match &ast.node_type {
11760            ASTNodeType::Function { name, args } => {
11761                let normalized = name.to_uppercase();
11762                let mut spellings = vec![(String::new(), normalized.clone())];
11763                let mut stripped = normalized.as_str();
11764                while let Some(rest) = ["_XLFN.", "_XLL.", "_XLWS."]
11765                    .iter()
11766                    .find_map(|prefix| stripped.strip_prefix(prefix))
11767                {
11768                    stripped = rest;
11769                    spellings.push((String::new(), stripped.to_string()));
11770                }
11771                let resolved = crate::function_registry::resolve("", name);
11772                let directly_changed = spellings.iter().any(|spelling| changed.contains(spelling))
11773                    || resolved.as_ref().is_some_and(|resolved| {
11774                        changed.contains(&(
11775                            resolved.namespace.clone(),
11776                            resolved.canonical_name.clone(),
11777                        ))
11778                    });
11779                directly_changed
11780                    || resolved.is_none()
11781                    || args
11782                        .iter()
11783                        .any(|arg| Self::ast_uses_changed_function(arg, changed))
11784            }
11785            ASTNodeType::Call { callee, args } => {
11786                Self::ast_uses_changed_function(callee, changed)
11787                    || args
11788                        .iter()
11789                        .any(|arg| Self::ast_uses_changed_function(arg, changed))
11790            }
11791            ASTNodeType::UnaryOp { expr, .. } => Self::ast_uses_changed_function(expr, changed),
11792            ASTNodeType::BinaryOp { left, right, .. } => {
11793                Self::ast_uses_changed_function(left, changed)
11794                    || Self::ast_uses_changed_function(right, changed)
11795            }
11796            ASTNodeType::Array(rows) => rows
11797                .iter()
11798                .flatten()
11799                .any(|node| Self::ast_uses_changed_function(node, changed)),
11800            _ => false,
11801        }
11802    }
11803
11804    fn ast_contains_function(ast: &ASTNode) -> bool {
11805        match &ast.node_type {
11806            ASTNodeType::Function { .. } => true,
11807            ASTNodeType::Call { callee, args } => {
11808                Self::ast_contains_function(callee) || args.iter().any(Self::ast_contains_function)
11809            }
11810            ASTNodeType::UnaryOp { expr, .. } => Self::ast_contains_function(expr),
11811            ASTNodeType::BinaryOp { left, right, .. } => {
11812                Self::ast_contains_function(left) || Self::ast_contains_function(right)
11813            }
11814            ASTNodeType::Array(rows) => rows.iter().flatten().any(Self::ast_contains_function),
11815            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => false,
11816        }
11817    }
11818
11819    fn structural_row_region(sheet_id: SheetId, start_row0: u32) -> Region {
11820        Region::rows_from(sheet_id, start_row0)
11821    }
11822
11823    fn structural_col_region(sheet_id: SheetId, start_col0: u32) -> Region {
11824        Region::cols_from(sheet_id, start_col0)
11825    }
11826
11827    #[cfg(test)]
11828    pub(crate) fn force_non_cycle_schedule_fallback_for_test(&mut self) {
11829        self.force_non_cycle_schedule_fallback_for_test = true;
11830    }
11831
11832    fn materialize_deferred_sheet_before_structural_edit(
11833        &mut self,
11834        sheet: &str,
11835    ) -> Result<(), crate::engine::EditorError> {
11836        if self.staged_formulas.contains_key(sheet) {
11837            self.build_graph_for_sheets([sheet])?;
11838        }
11839        Ok(())
11840    }
11841
11842    /// Refuse an insert of `count` rows (`rows`) or columns at 0-based
11843    /// `before0` that would push content off the grid, as Excel does; nothing
11844    /// is changed. Content is the Arrow extent (which may include trailing
11845    /// empty cells, so this can refuse conservatively) and every formula.
11846    fn check_insert_room(
11847        &self,
11848        sheet: &str,
11849        before0: u32,
11850        count: u32,
11851        rows: bool,
11852    ) -> Result<(), crate::engine::EditorError> {
11853        let limit = if rows {
11854            crate::engine::authority::geom::MAX_ROW
11855        } else {
11856            crate::engine::authority::geom::MAX_COL
11857        } + 1;
11858        let arrow = self.arrow_sheets.sheet(sheet).map_or(0, |a| {
11859            if rows {
11860                a.nrows
11861            } else {
11862                u32::try_from(a.columns.len()).unwrap_or(u32::MAX)
11863            }
11864        });
11865        let formulas = self.graph.sheet_id(sheet).map_or(0, |id| {
11866            self.graph
11867                .grid_vertices_in_sheet(id)
11868                .map(|(_, at)| if rows { at.row() } else { at.col() } + 1)
11869                .max()
11870                .unwrap_or(0)
11871        });
11872        let extent = arrow.max(formulas);
11873        let pushed_off = extent > before0 && extent.saturating_add(count) > limit;
11874        if count > limit || pushed_off {
11875            return Err(crate::engine::EditorError::Excel(
11876                ExcelError::new(ExcelErrorKind::Ref).with_message(format!(
11877                    "inserting {count} {} would push non-empty cells off the sheet",
11878                    if rows { "rows" } else { "columns" }
11879                )),
11880            ));
11881        }
11882        Ok(())
11883    }
11884
11885    fn structural_row_occupancy(
11886        &self,
11887        sheet: &str,
11888        sheet_id: SheetId,
11889    ) -> crate::engine::graph::StructuralOccupancy {
11890        if !self.graph.has_compressed_range_readers() {
11891            return crate::engine::graph::StructuralOccupancy::default();
11892        }
11893        let mut occupancy = self.graph.structural_occupancy(sheet_id);
11894        if let Some(arrow_sheet) = self.arrow_sheets.sheet(sheet) {
11895            occupancy.include_arrow_sheet(arrow_sheet);
11896            occupancy
11897        } else {
11898            // Missing Arrow state cannot prove an apparently empty column empty.
11899            crate::engine::graph::StructuralOccupancy::conservative()
11900        }
11901    }
11902
11903    /// Tear down projections in the old coordinate frame, before Arrow moves
11904    /// either generated cells or user cells. Recalculation installs fresh extents.
11905    fn clear_spills_before_structural_edit(
11906        &mut self,
11907        sheet: SheetId,
11908        start: u32,
11909        rows: bool,
11910        capture: Option<&mut MutationCapture>,
11911        undo: Option<&mut crate::engine::ArrowUndoBatch>,
11912    ) -> Result<Vec<CellRef>, crate::engine::EditorError> {
11913        let anchors = self.graph.spill_anchors_before_shift(sheet, start, rows);
11914        if anchors.is_empty() {
11915            return Ok(Vec::new());
11916        }
11917        let cells: Vec<_> = anchors
11918            .iter()
11919            .flat_map(|&anchor| self.graph.spill_cells_to_clear(anchor))
11920            .collect();
11921        if let Some(capture) = capture {
11922            let start_len = capture.len();
11923            self.edit_with_capture(capture, |editor| {
11924                for &anchor in &anchors {
11925                    editor.clear_spill_region(anchor);
11926                }
11927            })?;
11928            if let Some(undo) = undo {
11929                self.record_spill_ops_into_arrow_undo(undo, &capture.events()[start_len..]);
11930            }
11931        } else {
11932            for &anchor in &anchors {
11933                self.clear_spill_projection_and_mirror(anchor, None);
11934            }
11935        }
11936        self.graph.mark_dirty_many(&anchors);
11937        Ok(cells)
11938    }
11939
11940    /// Empty overlays mask old outputs compacted into base storage. Structural
11941    /// cache invalidation must not expose them as orphan literals.
11942    fn restore_shifted_spill_tombstones(
11943        &mut self,
11944        cells: Vec<CellRef>,
11945        start: u32,
11946        count: u32,
11947        rows: bool,
11948        insert: bool,
11949        mut undo: Option<&mut crate::engine::ArrowUndoBatch>,
11950    ) {
11951        for cell in cells {
11952            let old = if rows {
11953                cell.coord.row()
11954            } else {
11955                cell.coord.col()
11956            };
11957            let shifted = if old < start {
11958                old
11959            } else if insert {
11960                old.saturating_add(count)
11961            } else if old < start.saturating_add(count) {
11962                continue;
11963            } else {
11964                old - count
11965            };
11966            let (row, col) = if rows {
11967                (shifted, cell.coord.col())
11968            } else {
11969                (cell.coord.row(), shifted)
11970            };
11971            let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
11972            if let Some(undo) = undo.as_deref_mut() {
11973                let old = self.read_computed_overlay_cell(&sheet, row + 1, col + 1);
11974                undo.record_computed_cell(cell.sheet_id, row, col, old, Some(LiteralValue::Empty));
11975            }
11976            self.mirror_value_to_computed_overlay(&sheet, row + 1, col + 1, &LiteralValue::Empty);
11977        }
11978    }
11979
11980    fn structural_column_occupancy(&self) -> crate::engine::graph::StructuralOccupancy {
11981        // Arrow exposes occupied columns through chunk metadata and overlay maps,
11982        // but has no cheap occupied-row index. Column edits therefore deliberately
11983        // retain conservative cross-axis invalidation instead of scanning cells.
11984        crate::engine::graph::StructuralOccupancy::conservative()
11985    }
11986
11987    /// Insert rows (1-based) and mirror into Arrow store when enabled
11988    pub fn insert_rows(
11989        &mut self,
11990        sheet: &str,
11991        before: u32,
11992        count: u32,
11993    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
11994    {
11995        self.ensure_not_poisoned()?;
11996        if count == 0 {
11997            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
11998        }
11999        self.observe_function_semantic_epoch()
12000            .map_err(crate::engine::EditorError::Excel)?;
12001        use crate::engine::graph::editor::vertex_editor::VertexEditor;
12002        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
12003        let sheet_id = self.ensure_known_sheet_id(sheet)?;
12004        let before0 = before.saturating_sub(1);
12005        self.check_insert_room(sheet, before0, count, true)?;
12006        let affected_region = Self::structural_row_region(sheet_id, before0);
12007        let occupancy = self.structural_row_occupancy(sheet, sheet_id);
12008        let cleared_spill_cells =
12009            self.clear_spills_before_structural_edit(sheet_id, before0, true, None, None)?;
12010        let summary = {
12011            let mut editor =
12012                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
12013            editor.insert_rows(sheet_id, before0, count)?
12014        };
12015        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
12016            let before0 = before0 as usize;
12017            asheet.insert_rows(before0, count as usize);
12018        }
12019        self.purge_derived_formats_after_row(sheet_id, before0);
12020        self.mark_moved_formula_vertices_dirty(&summary);
12021        self.clear_computed_overlay_after_row(sheet, before0 as usize);
12022        self.restore_shifted_spill_tombstones(
12023            cleared_spill_cells,
12024            before0,
12025            count,
12026            true,
12027            true,
12028            None,
12029        );
12030        self.shift_row_visibility_insert(sheet_id, before0, count);
12031        self.record_structural_change(StructuralScope::Region(affected_region));
12032        self.mark_topology_edited();
12033        Ok(summary)
12034    }
12035
12036    /// Delete rows (1-based) and mirror into Arrow store when enabled
12037    pub fn delete_rows(
12038        &mut self,
12039        sheet: &str,
12040        start: u32,
12041        count: u32,
12042    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
12043    {
12044        self.ensure_not_poisoned()?;
12045        if count == 0 {
12046            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
12047        }
12048        self.observe_function_semantic_epoch()
12049            .map_err(crate::engine::EditorError::Excel)?;
12050        use crate::engine::graph::editor::vertex_editor::VertexEditor;
12051        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
12052        let sheet_id = self.ensure_known_sheet_id(sheet)?;
12053        let start0 = start.saturating_sub(1);
12054        let affected_region = Self::structural_row_region(sheet_id, start0);
12055        let occupancy = self.structural_row_occupancy(sheet, sheet_id);
12056        let cleared_spill_cells =
12057            self.clear_spills_before_structural_edit(sheet_id, start0, true, None, None)?;
12058        let summary = {
12059            let mut editor =
12060                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
12061            editor.delete_rows(sheet_id, start0, count)?
12062        };
12063        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
12064            let start0 = start0 as usize;
12065            asheet.delete_rows(start0, count as usize);
12066        }
12067        self.purge_derived_formats_after_row(sheet_id, start0);
12068        self.mark_moved_formula_vertices_dirty(&summary);
12069        self.clear_computed_overlay_after_row(sheet, start0 as usize);
12070        self.restore_shifted_spill_tombstones(
12071            cleared_spill_cells,
12072            start0,
12073            count,
12074            true,
12075            false,
12076            None,
12077        );
12078        self.shift_row_visibility_delete(sheet_id, start0, count);
12079        self.record_structural_change(StructuralScope::Region(affected_region));
12080        self.mark_topology_edited();
12081        Ok(summary)
12082    }
12083
12084    /// Insert columns (1-based) and mirror into Arrow store when enabled
12085    pub fn insert_columns(
12086        &mut self,
12087        sheet: &str,
12088        before: u32,
12089        count: u32,
12090    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
12091    {
12092        self.ensure_not_poisoned()?;
12093        if count == 0 {
12094            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
12095        }
12096        self.observe_function_semantic_epoch()
12097            .map_err(crate::engine::EditorError::Excel)?;
12098        use crate::engine::graph::editor::vertex_editor::VertexEditor;
12099        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
12100        let sheet_id = self.graph.sheet_id(sheet).ok_or(
12101            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
12102                name: sheet.to_string(),
12103                reason: "Unknown sheet".to_string(),
12104            },
12105        )?;
12106        let before0 = before.saturating_sub(1);
12107        self.check_insert_room(sheet, before0, count, false)?;
12108        let affected_region = Self::structural_col_region(sheet_id, before0);
12109        let occupancy = self.structural_column_occupancy();
12110        let cleared_spill_cells =
12111            self.clear_spills_before_structural_edit(sheet_id, before0, false, None, None)?;
12112        let summary = {
12113            let mut editor =
12114                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
12115            editor.insert_columns(sheet_id, before0, count)?
12116        };
12117        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
12118            let before0 = before0 as usize;
12119            asheet.insert_columns(before0, count as usize);
12120        }
12121        self.purge_derived_formats_after_col(sheet_id, before0);
12122        self.mark_moved_formula_vertices_dirty(&summary);
12123        self.clear_computed_overlay_after_col(sheet, before0 as usize);
12124        self.restore_shifted_spill_tombstones(
12125            cleared_spill_cells,
12126            before0,
12127            count,
12128            false,
12129            true,
12130            None,
12131        );
12132        self.record_structural_change(StructuralScope::Region(affected_region));
12133        self.mark_topology_edited();
12134        Ok(summary)
12135    }
12136
12137    /// Delete columns (1-based) and mirror into Arrow store when enabled
12138    pub fn delete_columns(
12139        &mut self,
12140        sheet: &str,
12141        start: u32,
12142        count: u32,
12143    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
12144    {
12145        self.ensure_not_poisoned()?;
12146        if count == 0 {
12147            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
12148        }
12149        self.observe_function_semantic_epoch()
12150            .map_err(crate::engine::EditorError::Excel)?;
12151        use crate::engine::graph::editor::vertex_editor::VertexEditor;
12152        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
12153        let sheet_id = self.graph.sheet_id(sheet).ok_or(
12154            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
12155                name: sheet.to_string(),
12156                reason: "Unknown sheet".to_string(),
12157            },
12158        )?;
12159        let start0 = start.saturating_sub(1);
12160        let affected_region = Self::structural_col_region(sheet_id, start0);
12161        let occupancy = self.structural_column_occupancy();
12162        let cleared_spill_cells =
12163            self.clear_spills_before_structural_edit(sheet_id, start0, false, None, None)?;
12164        let summary = {
12165            let mut editor =
12166                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
12167            editor.delete_columns(sheet_id, start0, count)?
12168        };
12169        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
12170            let start0 = start0 as usize;
12171            asheet.delete_columns(start0, count as usize);
12172        }
12173        self.purge_derived_formats_after_col(sheet_id, start0);
12174        self.mark_moved_formula_vertices_dirty(&summary);
12175        self.clear_computed_overlay_after_col(sheet, start0 as usize);
12176        self.restore_shifted_spill_tombstones(
12177            cleared_spill_cells,
12178            start0,
12179            count,
12180            false,
12181            false,
12182            None,
12183        );
12184        self.record_structural_change(StructuralScope::Region(affected_region));
12185        self.mark_topology_edited();
12186        Ok(summary)
12187    }
12188    /// Arrow-backed used row bounds across a column span (1-based inclusive cols).
12189    fn arrow_used_row_bounds(
12190        &self,
12191        sheet: &str,
12192        start_col: u32,
12193        end_col: u32,
12194    ) -> Option<(u32, u32)> {
12195        let a = self.sheet_store().sheet(sheet)?;
12196        if a.columns.is_empty() {
12197            return None;
12198        }
12199        let sc0 = start_col.saturating_sub(1) as usize;
12200        let ec0 = end_col.saturating_sub(1) as usize;
12201        let col_hi = a.columns.len().saturating_sub(1);
12202        if sc0 > col_hi {
12203            return None;
12204        }
12205        let ec0 = ec0.min(col_hi);
12206        // Pass-scoped cache with snapshot guard
12207        let snap = self.data_snapshot_id();
12208        let mut min_r0: Option<usize> = None;
12209        for ci in sc0..=ec0 {
12210            let sheet_id = self.graph.sheet_id(sheet)?;
12211            if let Some((Some(mv), _)) = self.row_bounds_cache.read().ok().and_then(|g| {
12212                g.as_ref()
12213                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
12214            }) {
12215                let mv = mv as usize;
12216                min_r0 = Some(min_r0.map(|m| m.min(mv)).unwrap_or(mv));
12217                continue;
12218            }
12219            // Compute and store
12220            let (min_c, max_c) = Self::scan_column_used_bounds(a, ci);
12221            if let Ok(mut g) = self.row_bounds_cache.write() {
12222                g.get_or_insert_with(|| RowBoundsCache::new(snap))
12223                    .put_row_bounds(sheet_id, ci, snap, (min_c, max_c));
12224            }
12225            if let Some(m) = min_c {
12226                min_r0 = Some(min_r0.map(|mm| mm.min(m as usize)).unwrap_or(m as usize));
12227            }
12228        }
12229        min_r0?;
12230        let mut max_r0: Option<usize> = None;
12231        for ci in sc0..=ec0 {
12232            let sheet_id = self.graph.sheet_id(sheet)?;
12233            if let Some((_, Some(mv))) = self.row_bounds_cache.read().ok().and_then(|g| {
12234                g.as_ref()
12235                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
12236            }) {
12237                let mv = mv as usize;
12238                max_r0 = Some(max_r0.map(|m| m.max(mv)).unwrap_or(mv));
12239                continue;
12240            }
12241            let (_min_c, max_c) = Self::scan_column_used_bounds(a, ci);
12242            if let Ok(mut g) = self.row_bounds_cache.write() {
12243                g.get_or_insert_with(|| RowBoundsCache::new(snap))
12244                    .put_row_bounds(sheet_id, ci, snap, (_min_c, max_c));
12245            }
12246            if let Some(m) = max_c {
12247                max_r0 = Some(max_r0.map(|mm| mm.max(m as usize)).unwrap_or(m as usize));
12248            }
12249        }
12250        match (min_r0, max_r0) {
12251            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
12252            _ => None,
12253        }
12254    }
12255
12256    fn scan_column_used_bounds(
12257        a: &crate::arrow_store::ArrowSheet,
12258        ci: usize,
12259    ) -> (Option<u32>, Option<u32>) {
12260        let col = &a.columns[ci];
12261
12262        // Min: scan dense chunks first, then sparse chunks in ascending index order.
12263        let mut min_r0: Option<u32> = None;
12264        for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
12265            let tags = chunk.type_tag.values();
12266            for (off, &t) in tags.iter().enumerate() {
12267                let overlay_non_empty = chunk
12268                    .overlay
12269                    .get(off)
12270                    .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12271                    .unwrap_or(false)
12272                    || chunk
12273                        .computed_overlay
12274                        .get(off)
12275                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12276                        .unwrap_or(false);
12277                if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
12278                    let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
12279                        break;
12280                    };
12281                    let row0 = chunk_start + off;
12282                    min_r0 = Some(row0 as u32);
12283                    break;
12284                }
12285            }
12286            if min_r0.is_some() {
12287                break;
12288            }
12289        }
12290        if min_r0.is_none() && !col.sparse_chunks.is_empty() {
12291            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
12292            sparse_idxs.sort_unstable();
12293            for chunk_idx in sparse_idxs {
12294                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
12295                    continue;
12296                };
12297                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
12298                    continue;
12299                };
12300                let tags = chunk.type_tag.values();
12301                for (off, &t) in tags.iter().enumerate() {
12302                    let overlay_non_empty = chunk
12303                        .overlay
12304                        .get(off)
12305                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12306                        .unwrap_or(false)
12307                        || chunk
12308                            .computed_overlay
12309                            .get(off)
12310                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12311                            .unwrap_or(false);
12312                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
12313                        let row0 = chunk_start + off;
12314                        min_r0 = Some(row0 as u32);
12315                        break;
12316                    }
12317                }
12318                if min_r0.is_some() {
12319                    break;
12320                }
12321            }
12322        }
12323
12324        // Max: scan sparse chunks in descending index order, then dense chunks in reverse.
12325        let mut max_r0: Option<u32> = None;
12326        if !col.sparse_chunks.is_empty() {
12327            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
12328            sparse_idxs.sort_unstable_by(|a, b| b.cmp(a));
12329            for chunk_idx in sparse_idxs {
12330                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
12331                    continue;
12332                };
12333                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
12334                    continue;
12335                };
12336                let tags = chunk.type_tag.values();
12337                for (rev_idx, &t) in tags.iter().enumerate().rev() {
12338                    let overlay_non_empty = chunk
12339                        .overlay
12340                        .get(rev_idx)
12341                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12342                        .unwrap_or(false)
12343                        || chunk
12344                            .computed_overlay
12345                            .get(rev_idx)
12346                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12347                            .unwrap_or(false);
12348                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
12349                        let row0 = chunk_start + rev_idx;
12350                        max_r0 = Some(row0 as u32);
12351                        break;
12352                    }
12353                }
12354                if max_r0.is_some() {
12355                    break;
12356                }
12357            }
12358        }
12359        if max_r0.is_none() {
12360            for (chunk_idx, chunk) in col.chunks.iter().enumerate().rev() {
12361                let tags = chunk.type_tag.values();
12362                for (rev_idx, &t) in tags.iter().enumerate().rev() {
12363                    let overlay_non_empty = chunk
12364                        .overlay
12365                        .get(rev_idx)
12366                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12367                        .unwrap_or(false)
12368                        || chunk
12369                            .computed_overlay
12370                            .get(rev_idx)
12371                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12372                            .unwrap_or(false);
12373                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
12374                        let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
12375                            break;
12376                        };
12377                        let row0 = chunk_start + rev_idx;
12378                        max_r0 = Some(row0 as u32);
12379                        break;
12380                    }
12381                }
12382                if max_r0.is_some() {
12383                    break;
12384                }
12385            }
12386        }
12387
12388        (min_r0, max_r0)
12389    }
12390
12391    /// Arrow-backed used column bounds across a row span (1-based inclusive rows).
12392    fn arrow_used_col_bounds(
12393        &self,
12394        sheet: &str,
12395        start_row: u32,
12396        end_row: u32,
12397    ) -> Option<(u32, u32)> {
12398        let a = self.sheet_store().sheet(sheet)?;
12399        if a.columns.is_empty() {
12400            return None;
12401        }
12402        let sr0 = start_row.saturating_sub(1) as usize;
12403        let er0 = end_row.saturating_sub(1) as usize;
12404        if sr0 > er0 {
12405            return None;
12406        }
12407        // Map start/end rows into chunk ranges
12408        // We will scan each column for any non-empty within [sr0..=er0]
12409        let mut min_c0: Option<usize> = None;
12410        let mut max_c0: Option<usize> = None;
12411        // Precompute chunk bounds for row range
12412        for (ci, col) in a.columns.iter().enumerate() {
12413            let mut any_in_range = false;
12414
12415            let scan_chunk = |chunk_idx: usize, chunk: &crate::arrow_store::ColumnChunk| -> bool {
12416                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
12417                    return false;
12418                };
12419                let chunk_len = chunk.type_tag.len();
12420                if chunk_len == 0 {
12421                    return false;
12422                }
12423                let chunk_end = chunk_start + chunk_len.saturating_sub(1);
12424                // check intersection
12425                if sr0 > chunk_end || er0 < chunk_start {
12426                    return false;
12427                }
12428                let start_off = sr0.max(chunk_start) - chunk_start;
12429                let end_off = er0.min(chunk_end) - chunk_start;
12430                let tags = chunk.type_tag.values();
12431                for off in start_off..=end_off {
12432                    let overlay_non_empty = chunk
12433                        .overlay
12434                        .get(off)
12435                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12436                        .unwrap_or(false)
12437                        || chunk
12438                            .computed_overlay
12439                            .get(off)
12440                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
12441                            .unwrap_or(false);
12442                    if overlay_non_empty || tags[off] != crate::arrow_store::TypeTag::Empty as u8 {
12443                        return true;
12444                    }
12445                }
12446                false
12447            };
12448
12449            for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
12450                if scan_chunk(chunk_idx, chunk) {
12451                    any_in_range = true;
12452                    break;
12453                }
12454            }
12455
12456            if !any_in_range && !col.sparse_chunks.is_empty() {
12457                for (&chunk_idx, chunk) in col.sparse_chunks.iter() {
12458                    if scan_chunk(chunk_idx, chunk) {
12459                        any_in_range = true;
12460                        break;
12461                    }
12462                }
12463            }
12464
12465            if any_in_range {
12466                min_c0 = Some(min_c0.map(|m| m.min(ci)).unwrap_or(ci));
12467                max_c0 = Some(max_c0.map(|m| m.max(ci)).unwrap_or(ci));
12468            }
12469        }
12470        match (min_c0, max_c0) {
12471            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
12472            _ => None,
12473        }
12474    }
12475
12476    fn formula_row_bounds_for_columns(
12477        &self,
12478        sheet: &str,
12479        start_col: u32,
12480        end_col: u32,
12481    ) -> Option<(u32, u32)> {
12482        let sheet_id = self.graph.sheet_id(sheet)?;
12483        let sc0 = start_col.saturating_sub(1);
12484        let ec0 = end_col.saturating_sub(1);
12485        let mut min_r0: Option<u32> = None;
12486        let mut max_r0: Option<u32> = None;
12487
12488        if self.graph.sheet_index(sheet_id).is_some() {
12489            for vid in self.graph.vertices_in_cols(sheet_id, sc0, ec0) {
12490                if !matches!(
12491                    self.graph.get_vertex_kind(vid),
12492                    VertexKind::FormulaScalar | VertexKind::FormulaArray
12493                ) {
12494                    continue;
12495                }
12496                let Some(row0) = self.graph.vertex_grid_addr(vid).map(|addr| addr.row()) else {
12497                    continue;
12498                };
12499                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
12500                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
12501            }
12502        } else {
12503            for (vid, coord) in self.graph.grid_vertices_in_sheet(sheet_id) {
12504                if !matches!(
12505                    self.graph.get_vertex_kind(vid),
12506                    VertexKind::FormulaScalar | VertexKind::FormulaArray
12507                ) {
12508                    continue;
12509                }
12510                let col0 = coord.col();
12511                if col0 < sc0 || col0 > ec0 {
12512                    continue;
12513                }
12514                let row0 = coord.row();
12515                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
12516                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
12517            }
12518        }
12519
12520        match (min_r0, max_r0) {
12521            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
12522            _ => None,
12523        }
12524    }
12525
12526    fn formula_col_bounds_for_rows(
12527        &self,
12528        sheet: &str,
12529        start_row: u32,
12530        end_row: u32,
12531    ) -> Option<(u32, u32)> {
12532        let sheet_id = self.graph.sheet_id(sheet)?;
12533        let sr0 = start_row.saturating_sub(1);
12534        let er0 = end_row.saturating_sub(1);
12535        let mut min_c0: Option<u32> = None;
12536        let mut max_c0: Option<u32> = None;
12537
12538        if self.graph.sheet_index(sheet_id).is_some() {
12539            for vid in self.graph.vertices_in_rows(sheet_id, sr0, er0) {
12540                if !matches!(
12541                    self.graph.get_vertex_kind(vid),
12542                    VertexKind::FormulaScalar | VertexKind::FormulaArray
12543                ) {
12544                    continue;
12545                }
12546                let Some(col0) = self.graph.vertex_grid_addr(vid).map(|addr| addr.col()) else {
12547                    continue;
12548                };
12549                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
12550                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
12551            }
12552        } else {
12553            for (vid, coord) in self.graph.grid_vertices_in_sheet(sheet_id) {
12554                if !matches!(
12555                    self.graph.get_vertex_kind(vid),
12556                    VertexKind::FormulaScalar | VertexKind::FormulaArray
12557                ) {
12558                    continue;
12559                }
12560                let row0 = coord.row();
12561                if row0 < sr0 || row0 > er0 {
12562                    continue;
12563                }
12564                let col0 = coord.col();
12565                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
12566                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
12567            }
12568        }
12569
12570        match (min_c0, max_c0) {
12571            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
12572            _ => None,
12573        }
12574    }
12575
12576    fn union_used_bounds(
12577        first: Option<(u32, u32)>,
12578        second: Option<(u32, u32)>,
12579    ) -> Option<(u32, u32)> {
12580        match (first, second) {
12581            (Some((a0, b0)), Some((a1, b1))) => Some((a0.min(a1), b0.max(b1))),
12582            (Some(bounds), None) | (None, Some(bounds)) => Some(bounds),
12583            (None, None) => None,
12584        }
12585    }
12586
12587    /// Mirror a single cell value into the Arrow overlay if enabled.
12588    /// Handles capacity growth, per-chunk overlay set, and heuristic compaction.
12589    fn mirror_value_to_overlay(&mut self, sheet: &str, row: u32, col: u32, value: &LiteralValue) {
12590        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
12591            return;
12592        }
12593        if self.arrow_sheets.sheet(sheet).is_none() {
12594            self.arrow_sheets
12595                .sheets
12596                .push(crate::arrow_store::ArrowSheet {
12597                    name: std::sync::Arc::<str>::from(sheet),
12598                    date_system: self.config.date_system,
12599                    columns: Vec::new(),
12600                    nrows: 0,
12601                    chunk_starts: Vec::new(),
12602                    chunk_rows: 32 * 1024,
12603                });
12604        }
12605
12606        let row0 = row.saturating_sub(1) as usize;
12607        let col0 = col.saturating_sub(1) as usize;
12608
12609        let asheet = self
12610            .arrow_sheets
12611            .sheet_mut(sheet)
12612            .expect("ArrowSheet must exist");
12613
12614        let cur_cols = asheet.columns.len();
12615        if col0 >= cur_cols {
12616            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12617        }
12618
12619        if row0 >= asheet.nrows as usize {
12620            if asheet.columns.is_empty() {
12621                asheet.insert_columns(0, 1);
12622            }
12623            asheet.ensure_row_capacity(row0 + 1);
12624        }
12625        if let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) {
12626            let ov =
12627                crate::arrow_store::OverlayValue::from_literal_value(value, asheet.date_system);
12628            let computed_delta = if let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) {
12629                let _ = ch.overlay.set(in_off, ov);
12630                let format = match value {
12631                    LiteralValue::Date(_) => Some(crate::format::FormatId::DATE),
12632                    LiteralValue::DateTime(_) => Some(crate::format::FormatId::DATETIME),
12633                    LiteralValue::Time(_) => Some(crate::format::FormatId::TIME),
12634                    LiteralValue::Duration(_) => Some(crate::format::FormatId::DURATION),
12635                    _ => None,
12636                };
12637                ch.overlay.set_format(in_off, format);
12638                // A user edit must invalidate any computed (formula/spill) overlay entry at
12639                // this cell. Otherwise, if the delta overlay later compacts into the base lanes
12640                // (clearing `overlay`), a stale `computed_overlay=Empty` could incorrectly mask
12641                // the edited base value under the read cascade.
12642                ch.computed_overlay.remove(in_off)
12643            } else {
12644                return;
12645            };
12646            // Heuristic compaction: > len/50 or > 1024
12647            let abs_threshold = 1024usize;
12648            let frac_den = 50usize;
12649            let freed = asheet.maybe_compact_chunk(col0, ch_idx, abs_threshold, frac_den);
12650            if freed > 0 {
12651                self.overlay_compactions = self.overlay_compactions.saturating_add(1);
12652            }
12653            self.adjust_computed_overlay_bytes(computed_delta);
12654        }
12655    }
12656
12657    /// Remove a delta-overlay entry for a single cell (if present).
12658    ///
12659    /// This is used when transitioning a cell to a formula so that any previous user-edit overlay
12660    /// does not continue to mask computed overlay outputs.
12661    fn clear_delta_overlay_cell(&mut self, sheet: &str, row: u32, col: u32) {
12662        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
12663            return;
12664        }
12665        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
12666            return;
12667        };
12668        let row0 = row.saturating_sub(1) as usize;
12669        let col0 = col.saturating_sub(1) as usize;
12670        if row0 >= asheet.nrows as usize {
12671            return;
12672        }
12673        if col0 >= asheet.columns.len() {
12674            return;
12675        }
12676        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
12677            return;
12678        };
12679        if let Some(ch) = asheet.columns[col0].chunk_mut(ch_idx) {
12680            let _ = ch.overlay.remove(in_off);
12681        }
12682    }
12683
12684    /// Structural invalidation also removes caches of foreign formulas. Keep
12685    /// those removals in the operation's coordinate frame for exact rollback.
12686    fn journal_structural_computed_clear(
12687        &self,
12688        sheet: &str,
12689        start: usize,
12690        rows: bool,
12691        undo: &mut crate::engine::ArrowUndoBatch,
12692    ) {
12693        let Some(sheet_id) = self.graph.sheet_id(sheet) else {
12694            return;
12695        };
12696        let Some(asheet) = self.arrow_sheets.sheet(sheet) else {
12697            return;
12698        };
12699        for (col, column) in asheet.columns.iter().enumerate() {
12700            if !rows && col < start {
12701                continue;
12702            }
12703            for (chunk, data) in column.chunks.iter().enumerate().chain(
12704                column
12705                    .sparse_chunks
12706                    .iter()
12707                    .map(|(chunk, data)| (*chunk, data)),
12708            ) {
12709                let Some(&row0) = asheet.chunk_starts.get(chunk) else {
12710                    continue;
12711                };
12712                for (off, value) in data.computed_overlay.iter() {
12713                    let row = row0.saturating_add(off);
12714                    if rows && row < start {
12715                        continue;
12716                    }
12717                    undo.record_computed_cell(
12718                        sheet_id,
12719                        row as u32,
12720                        col as u32,
12721                        Some(value.to_literal_for(asheet.date_system)),
12722                        None,
12723                    );
12724                }
12725            }
12726        }
12727    }
12728
12729    fn clear_computed_overlay_after_row(&mut self, sheet: &str, start_row0: usize) {
12730        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
12731            return;
12732        }
12733
12734        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
12735            return;
12736        };
12737        if start_row0 >= asheet.nrows as usize {
12738            return;
12739        }
12740
12741        let starts = asheet.chunk_starts.clone();
12742        let nrows = asheet.nrows as usize;
12743        let mut delta = 0isize;
12744        for col in &mut asheet.columns {
12745            for (chunk_idx, ch) in col.chunks.iter_mut().enumerate() {
12746                let Some(&chunk_start) = starts.get(chunk_idx) else {
12747                    continue;
12748                };
12749                let chunk_end = starts
12750                    .get(chunk_idx + 1)
12751                    .copied()
12752                    .unwrap_or(nrows)
12753                    .min(chunk_start.saturating_add(ch.len()));
12754                if chunk_end <= start_row0 {
12755                    continue;
12756                }
12757                if chunk_start >= start_row0 {
12758                    delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
12759                } else {
12760                    let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
12761                    delta = delta
12762                        .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
12763                }
12764            }
12765
12766            for (chunk_idx, ch) in &mut col.sparse_chunks {
12767                let Some(&chunk_start) = starts.get(*chunk_idx) else {
12768                    continue;
12769                };
12770                let chunk_end = starts
12771                    .get(*chunk_idx + 1)
12772                    .copied()
12773                    .unwrap_or(nrows)
12774                    .min(chunk_start.saturating_add(ch.len()));
12775                if chunk_end <= start_row0 {
12776                    continue;
12777                }
12778                if chunk_start >= start_row0 {
12779                    delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
12780                } else {
12781                    let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
12782                    delta = delta
12783                        .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
12784                }
12785            }
12786        }
12787        self.adjust_computed_overlay_bytes(delta);
12788    }
12789
12790    fn clear_computed_overlay_after_col(&mut self, sheet: &str, start_col0: usize) {
12791        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
12792            return;
12793        }
12794
12795        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
12796            return;
12797        };
12798        if start_col0 >= asheet.columns.len() {
12799            return;
12800        }
12801
12802        let mut delta = 0isize;
12803        for col in asheet.columns.iter_mut().skip(start_col0) {
12804            for ch in &mut col.chunks {
12805                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
12806            }
12807            for ch in col.sparse_chunks.values_mut() {
12808                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
12809            }
12810        }
12811        self.adjust_computed_overlay_bytes(delta);
12812    }
12813
12814    #[inline]
12815    fn literal_to_overlay_value(
12816        value: &LiteralValue,
12817        date_system: crate::engine::DateSystem,
12818    ) -> crate::arrow_store::OverlayValue {
12819        crate::arrow_store::OverlayValue::from_literal_value(value, date_system)
12820    }
12821
12822    fn arrow_sheet_date_system(&self, sheet: &str) -> crate::engine::DateSystem {
12823        self.arrow_sheets
12824            .sheet(sheet)
12825            .map(|sheet| sheet.date_system)
12826            .unwrap_or(self.config.date_system)
12827    }
12828
12829    /// Read a single cell's delta overlay entry (if present), preserving the distinction between
12830    /// absent and explicit `Empty`.
12831    fn read_delta_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
12832        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
12833            return None;
12834        }
12835        let asheet = self.arrow_sheets.sheet(sheet)?;
12836        let row0 = row.saturating_sub(1) as usize;
12837        let col0 = col.saturating_sub(1) as usize;
12838        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
12839            return None;
12840        }
12841        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
12842        let ch = asheet.columns[col0].chunk(ch_idx)?;
12843        ch.overlay
12844            .get_scalar(in_off)
12845            .map(|ov| ov.to_literal_for(asheet.date_system))
12846    }
12847
12848    /// Read a single cell's computed overlay entry (if present), preserving the distinction
12849    /// between absent and explicit `Empty`.
12850    fn read_computed_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
12851        if !(self.config.arrow_storage_enabled
12852            && self.config.delta_overlay_enabled
12853            && self.config.write_formula_overlay_enabled)
12854        {
12855            return None;
12856        }
12857        let asheet = self.arrow_sheets.sheet(sheet)?;
12858        let row0 = row.saturating_sub(1) as usize;
12859        let col0 = col.saturating_sub(1) as usize;
12860        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
12861            return None;
12862        }
12863        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
12864        let ch = asheet.columns[col0].chunk(ch_idx)?;
12865        ch.computed_overlay
12866            .get_scalar(in_off)
12867            .map(|ov| ov.to_literal_for(asheet.date_system))
12868    }
12869
12870    fn set_delta_overlay_cell_raw(
12871        &mut self,
12872        sheet: &str,
12873        row: u32,
12874        col: u32,
12875        value: Option<LiteralValue>,
12876    ) {
12877        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
12878            return;
12879        }
12880
12881        self.ensure_arrow_sheet(sheet);
12882        let date_system = self.arrow_sheet_date_system(sheet);
12883        let ov_opt = value
12884            .as_ref()
12885            .map(|value| Self::literal_to_overlay_value(value, date_system));
12886        let row0 = row.saturating_sub(1) as usize;
12887        let col0 = col.saturating_sub(1) as usize;
12888        let asheet = self
12889            .arrow_sheets
12890            .sheet_mut(sheet)
12891            .expect("ArrowSheet must exist");
12892
12893        let cur_cols = asheet.columns.len();
12894        if col0 >= cur_cols {
12895            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12896        }
12897        if row0 >= asheet.nrows as usize {
12898            if asheet.columns.is_empty() {
12899                asheet.insert_columns(0, 1);
12900            }
12901            asheet.ensure_row_capacity(row0 + 1);
12902        }
12903
12904        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
12905            return;
12906        };
12907        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
12908            return;
12909        };
12910
12911        if let Some(ov) = ov_opt {
12912            let _ = ch.overlay.set(in_off, ov);
12913        } else {
12914            let _ = ch.overlay.remove(in_off);
12915        }
12916    }
12917
12918    fn set_computed_overlay_cell_raw(
12919        &mut self,
12920        sheet: &str,
12921        row: u32,
12922        col: u32,
12923        value: Option<LiteralValue>,
12924    ) {
12925        if !(self.config.arrow_storage_enabled
12926            && self.config.delta_overlay_enabled
12927            && self.config.write_formula_overlay_enabled)
12928        {
12929            return;
12930        }
12931
12932        self.ensure_arrow_sheet(sheet);
12933        let date_system = self.arrow_sheet_date_system(sheet);
12934        let ov_opt = value
12935            .as_ref()
12936            .map(|value| Self::literal_to_overlay_value(value, date_system));
12937        let row0 = row.saturating_sub(1) as usize;
12938        let col0 = col.saturating_sub(1) as usize;
12939        let asheet = self
12940            .arrow_sheets
12941            .sheet_mut(sheet)
12942            .expect("ArrowSheet must exist");
12943
12944        let cur_cols = asheet.columns.len();
12945        if col0 >= cur_cols {
12946            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12947        }
12948        if row0 >= asheet.nrows as usize {
12949            if asheet.columns.is_empty() {
12950                asheet.insert_columns(0, 1);
12951            }
12952            asheet.ensure_row_capacity(row0 + 1);
12953        }
12954
12955        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
12956            return;
12957        };
12958        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
12959            return;
12960        };
12961
12962        let delta = if let Some(ov) = ov_opt {
12963            ch.computed_overlay.set(in_off, ov)
12964        } else {
12965            ch.computed_overlay.remove(in_off)
12966        };
12967        self.adjust_computed_overlay_bytes(delta);
12968    }
12969
12970    fn apply_arrow_undo_batch(&mut self, batch: &crate::engine::ArrowUndoBatch, undo: bool) {
12971        use crate::engine::ArrowOp;
12972
12973        let iter: Box<dyn Iterator<Item = &ArrowOp>> = if undo {
12974            Box::new(batch.ops.iter().rev())
12975        } else {
12976            Box::new(batch.ops.iter())
12977        };
12978
12979        for op in iter {
12980            match op {
12981                ArrowOp::SetDeltaCell {
12982                    sheet_id,
12983                    row0,
12984                    col0,
12985                    old,
12986                    new,
12987                } => {
12988                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
12989                    // Graph undo has already restored projection ownership.
12990                    // A prior write in the same action may have cleared the
12991                    // spill before this cell's old delta was captured. Such
12992                    // deltas (including Empty) must not mask its snapshot.
12993                    let restored_projection = undo
12994                        && self
12995                            .graph
12996                            .spill_registry_anchor_for_cell(CellRef::new(
12997                                *sheet_id,
12998                                Coord::new(*row0, *col0, true, true),
12999                            ))
13000                            .is_some();
13001                    let v = if restored_projection {
13002                        None
13003                    } else if undo {
13004                        old.clone()
13005                    } else {
13006                        new.clone()
13007                    };
13008                    self.set_delta_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
13009                }
13010                ArrowOp::SetComputedCell {
13011                    sheet_id,
13012                    row0,
13013                    col0,
13014                    old,
13015                    new,
13016                } => {
13017                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
13018                    let v = if undo { old.clone() } else { new.clone() };
13019                    self.set_computed_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
13020                }
13021                ArrowOp::RestoreComputedRect {
13022                    sheet_id,
13023                    sr0,
13024                    sc0,
13025                    er0,
13026                    ec0,
13027                    old,
13028                    new,
13029                } => {
13030                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
13031                    let vals = if undo { old } else { new };
13032                    let height = (*er0).saturating_sub(*sr0) as usize + 1;
13033                    let width = (*ec0).saturating_sub(*sc0) as usize + 1;
13034                    for r in 0..height {
13035                        for c in 0..width {
13036                            let v = vals
13037                                .get(r)
13038                                .and_then(|row| row.get(c))
13039                                .cloned()
13040                                .unwrap_or(LiteralValue::Empty);
13041                            self.set_computed_overlay_cell_raw(
13042                                &sheet,
13043                                *sr0 + 1 + r as u32,
13044                                *sc0 + 1 + c as u32,
13045                                Some(v),
13046                            );
13047                        }
13048                    }
13049                }
13050                ArrowOp::InsertRows {
13051                    sheet_id,
13052                    before0,
13053                    count,
13054                } => {
13055                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
13056                    self.ensure_arrow_sheet(&sheet);
13057                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
13058                        if undo {
13059                            asheet.delete_rows(*before0 as usize, *count as usize);
13060                        } else {
13061                            asheet.insert_rows(*before0 as usize, *count as usize);
13062                        }
13063                    }
13064                    self.purge_derived_formats_after_row(*sheet_id, *before0);
13065                }
13066                ArrowOp::InsertCols {
13067                    sheet_id,
13068                    before0,
13069                    count,
13070                } => {
13071                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
13072                    self.ensure_arrow_sheet(&sheet);
13073                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
13074                        if undo {
13075                            asheet.delete_columns(*before0 as usize, *count as usize);
13076                        } else {
13077                            asheet.insert_columns(*before0 as usize, *count as usize);
13078                        }
13079                    }
13080                    self.purge_derived_formats_after_col(*sheet_id, *before0);
13081                }
13082            }
13083        }
13084    }
13085
13086    fn record_spill_ops_into_arrow_undo(
13087        &mut self,
13088        undo: &mut crate::engine::ArrowUndoBatch,
13089        events: &[crate::engine::ChangeEvent],
13090    ) {
13091        use crate::engine::ChangeEvent;
13092        use formualizer_common::LiteralValue;
13093
13094        #[allow(clippy::type_complexity)]
13095        let rect_from_snapshot =
13096            |snap: &crate::engine::graph::editor::change_log::SpillSnapshot|
13097             -> Option<(SheetId, u32, u32, u32, u32, Vec<Vec<LiteralValue>>)> {
13098                if snap.target_cells.is_empty() {
13099                    return None;
13100                }
13101                let sheet_id = snap.target_cells[0].sheet_id;
13102                let sr0 = snap.target_cells[0].coord.row();
13103                let sc0 = snap.target_cells[0].coord.col();
13104                if snap.values.is_empty() || snap.values[0].is_empty() {
13105                    return None;
13106                }
13107                let h = snap.values.len() as u32;
13108                let w = snap.values[0].len() as u32;
13109                let er0 = sr0.saturating_add(h.saturating_sub(1));
13110                let ec0 = sc0.saturating_add(w.saturating_sub(1));
13111                Some((sheet_id, sr0, sc0, er0, ec0, snap.values.clone()))
13112            };
13113
13114        for ev in events {
13115            match ev {
13116                ChangeEvent::SpillCommitted { old, new, .. } => {
13117                    if let Some((sid, sr0, sc0, er0, ec0, new_vals)) = rect_from_snapshot(new) {
13118                        let old_vals = if let Some(old_snap) = old {
13119                            rect_from_snapshot(old_snap)
13120                                .map(|(_, _, _, _, _, v)| v)
13121                                .unwrap_or_else(|| {
13122                                    vec![
13123                                        vec![LiteralValue::Empty; new_vals[0].len()];
13124                                        new_vals.len()
13125                                    ]
13126                                })
13127                        } else {
13128                            vec![vec![LiteralValue::Empty; new_vals[0].len()]; new_vals.len()]
13129                        };
13130                        undo.record_restore_computed_rect(
13131                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
13132                        );
13133                    }
13134                }
13135                ChangeEvent::SpillCleared { old, .. } => {
13136                    let width = old.values.first().map_or(0, Vec::len);
13137                    if old.target_cells.len() != old.values.len().saturating_mul(width) {
13138                        if let Some(first) = old.target_cells.first() {
13139                            for cell in &old.target_cells {
13140                                let r = (cell.coord.row() - first.coord.row()) as usize;
13141                                let c = (cell.coord.col() - first.coord.col()) as usize;
13142                                let value = old
13143                                    .values
13144                                    .get(r)
13145                                    .and_then(|row| row.get(c))
13146                                    .cloned()
13147                                    .unwrap_or(LiteralValue::Empty);
13148                                undo.record_computed_cell(
13149                                    cell.sheet_id,
13150                                    cell.coord.row(),
13151                                    cell.coord.col(),
13152                                    Some(value),
13153                                    Some(LiteralValue::Empty),
13154                                );
13155                            }
13156                        }
13157                        continue;
13158                    }
13159                    if let Some((sid, sr0, sc0, er0, ec0, old_vals)) = rect_from_snapshot(old) {
13160                        let new_vals =
13161                            vec![vec![LiteralValue::Empty; old_vals[0].len()]; old_vals.len()];
13162                        undo.record_restore_computed_rect(
13163                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
13164                        );
13165                    }
13166                }
13167                _ => {}
13168            }
13169        }
13170    }
13171
13172    /// Mirror a value into the computed overlay (formula/spill outputs).
13173    ///
13174    /// This path is subject to `EvalConfig.max_overlay_memory_bytes`.
13175    /// If the cap is exceeded, computed overlays are compacted into base lanes.
13176    fn mirror_value_to_computed_overlay(
13177        &mut self,
13178        sheet: &str,
13179        row: u32,
13180        col: u32,
13181        value: &LiteralValue,
13182    ) {
13183        if !(self.config.arrow_storage_enabled
13184            && self.config.delta_overlay_enabled
13185            && self.config.write_formula_overlay_enabled)
13186        {
13187            return;
13188        }
13189        if self.computed_overlay_mirroring_disabled {
13190            return;
13191        }
13192
13193        let date_system = self.arrow_sheet_date_system(sheet);
13194        let ov = Self::literal_to_overlay_value(value, date_system);
13195        self.write_computed_overlay_value_0based(
13196            sheet,
13197            row.saturating_sub(1),
13198            col.saturating_sub(1),
13199            ov,
13200        );
13201    }
13202
13203    fn record_derived_format(&self, vertex_id: VertexId, format: Option<crate::format::FormatId>) {
13204        if let Some(cell) = self.graph.get_cell_ref(vertex_id) {
13205            self.record_derived_format_at(cell, format);
13206        }
13207    }
13208
13209    fn record_derived_format_at(&self, cell: CellRef, format: Option<crate::format::FormatId>) {
13210        #[cfg(test)]
13211        self.derived_format_operations_for_test
13212            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13213        let format = format.filter(|id| *id != crate::format::FormatId::GENERAL);
13214        self.derived_formats.set(cell, format);
13215    }
13216
13217    fn clear_cell_format_state(&mut self, sheet: &str, cell: CellRef) {
13218        self.derived_formats.set(cell, None);
13219        if let Some(arrow) = self.arrow_sheets.sheet_mut(sheet) {
13220            arrow.clear_format(cell.coord.row() as usize, cell.coord.col() as usize);
13221        }
13222    }
13223
13224    fn clear_logged_cell_format_states(&mut self, events: &[ChangeEvent]) {
13225        let cells = events
13226            .iter()
13227            .filter_map(|event| match event {
13228                ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
13229                    Some(*addr)
13230                }
13231                _ => None,
13232            })
13233            .collect::<FxHashSet<_>>();
13234        for cell in cells {
13235            let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
13236            self.clear_cell_format_state(&sheet, cell);
13237        }
13238    }
13239
13240    fn purge_derived_formats_after_row(&mut self, sheet_id: SheetId, start0: u32) {
13241        self.derived_formats
13242            .retain(|cell| cell.sheet_id != sheet_id || cell.coord.row() < start0);
13243    }
13244
13245    fn purge_derived_formats_after_col(&mut self, sheet_id: SheetId, start0: u32) {
13246        self.derived_formats
13247            .retain(|cell| cell.sheet_id != sheet_id || cell.coord.col() < start0);
13248    }
13249
13250    fn purge_derived_formats_for_sheet(&mut self, sheet_id: SheetId) {
13251        self.derived_formats
13252            .retain(|cell| cell.sheet_id != sheet_id);
13253    }
13254
13255    #[cfg(test)]
13256    pub(crate) fn debug_computed_overlay_format_0based(
13257        &self,
13258        sheet: &str,
13259        row0: u32,
13260        col0: u32,
13261    ) -> Option<crate::format::FormatId> {
13262        let sheet = self.arrow_sheets.sheet(sheet)?;
13263        let (chunk_idx, row_in_chunk) = sheet.chunk_of_row(row0 as usize)?;
13264        sheet
13265            .columns
13266            .get(col0 as usize)?
13267            .chunk(chunk_idx)?
13268            .computed_overlay
13269            .get_format(row_in_chunk)
13270    }
13271
13272    #[cfg(test)]
13273    pub(crate) fn debug_computed_overlay_chunk_has_formats_0based(
13274        &self,
13275        sheet: &str,
13276        row0: u32,
13277        col0: u32,
13278    ) -> bool {
13279        let Some(sheet) = self.arrow_sheets.sheet(sheet) else {
13280            return false;
13281        };
13282        let Some((chunk_idx, _)) = sheet.chunk_of_row(row0 as usize) else {
13283            return false;
13284        };
13285        sheet
13286            .columns
13287            .get(col0 as usize)
13288            .and_then(|column| column.chunk(chunk_idx))
13289            .is_some_and(|chunk| chunk.computed_overlay.has_formats())
13290    }
13291
13292    #[cfg(test)]
13293    pub(crate) fn debug_clear_derived_format_0based(&mut self, sheet: &str, row0: u32, col0: u32) {
13294        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
13295            self.derived_formats
13296                .set(CellRef::new_absolute(sheet_id, row0, col0), None);
13297        }
13298    }
13299
13300    #[cfg(test)]
13301    pub(crate) fn debug_record_derived_format_0based(
13302        &self,
13303        sheet: &str,
13304        row0: u32,
13305        col0: u32,
13306        format: Option<crate::format::FormatId>,
13307    ) {
13308        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
13309            self.record_derived_format_at(CellRef::new_absolute(sheet_id, row0, col0), format);
13310        }
13311    }
13312
13313    #[cfg(test)]
13314    pub(crate) fn debug_derived_format_0based(
13315        &self,
13316        sheet: &str,
13317        row0: u32,
13318        col0: u32,
13319    ) -> Option<crate::format::FormatId> {
13320        let sheet_id = self.graph.sheet_id(sheet)?;
13321        self.derived_formats
13322            .get(&CellRef::new_absolute(sheet_id, row0, col0))
13323    }
13324
13325    #[cfg(test)]
13326    pub(crate) fn debug_reset_format_write_operation_counts(&mut self) {
13327        self.derived_format_operations_for_test
13328            .store(0, std::sync::atomic::Ordering::Relaxed);
13329        self.computed_overlay_set_explicit_entry_operations_for_test = 0;
13330        self.computed_overlay_stale_clear_range_effects_for_test = 0;
13331        self.computed_overlay_stale_clear_offset_attempts_for_test = 0;
13332        self.computed_format_vector_allocations_for_test
13333            .store(0, std::sync::atomic::Ordering::Relaxed);
13334    }
13335
13336    #[cfg(test)]
13337    pub(crate) fn debug_format_write_operation_counts(&self) -> (u64, u64, u64, u64, u64) {
13338        (
13339            self.derived_format_operations_for_test
13340                .load(std::sync::atomic::Ordering::Relaxed),
13341            self.computed_overlay_set_explicit_entry_operations_for_test,
13342            self.computed_format_vector_allocations_for_test
13343                .load(std::sync::atomic::Ordering::Relaxed),
13344            self.computed_overlay_stale_clear_range_effects_for_test,
13345            self.computed_overlay_stale_clear_offset_attempts_for_test,
13346        )
13347    }
13348
13349    fn write_computed_overlay_format_0based(
13350        &mut self,
13351        sheet: &str,
13352        row0: u32,
13353        col0: u32,
13354        format: Option<crate::format::FormatId>,
13355    ) {
13356        self.ensure_arrow_sheet(sheet);
13357        let (row0, col0) = (row0 as usize, col0 as usize);
13358        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
13359            return;
13360        };
13361        if col0 >= asheet.columns.len() {
13362            asheet.insert_columns(asheet.columns.len(), col0 + 1 - asheet.columns.len());
13363        }
13364        if row0 >= asheet.nrows as usize {
13365            asheet.ensure_row_capacity(row0 + 1);
13366        }
13367        let Some((chunk, offset)) = asheet.chunk_of_row(row0) else {
13368            return;
13369        };
13370        if let Some(chunk) = asheet.ensure_column_chunk_mut(col0, chunk) {
13371            chunk.computed_overlay.set_format(offset, format);
13372        }
13373    }
13374
13375    /// One unbuffered computed write of `value` and its derived `format` at
13376    /// `cell` (a single sheet lookup; the same writes as
13377    /// `write_computed_overlay_value_0based` then
13378    /// `write_computed_overlay_format_0based`).
13379    fn write_computed_cell_0based(
13380        &mut self,
13381        cell: CellRef,
13382        value: &LiteralValue,
13383        format: Option<crate::format::FormatId>,
13384    ) {
13385        if !(self.config.arrow_storage_enabled
13386            && self.config.delta_overlay_enabled
13387            && self.config.write_formula_overlay_enabled)
13388            || self.computed_overlay_mirroring_disabled
13389        {
13390            return;
13391        }
13392        if self.arrow_sheet_by_id(cell.sheet_id).is_none() {
13393            let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
13394            self.ensure_arrow_sheet(&sheet);
13395        }
13396        let (row0, col0) = (cell.coord.row() as usize, cell.coord.col() as usize);
13397        let Some(asheet) = self.arrow_sheet_by_id_mut(cell.sheet_id) else {
13398            return;
13399        };
13400        let ov = Self::literal_to_overlay_value(value, asheet.date_system);
13401        let cur_cols = asheet.columns.len();
13402        if col0 >= cur_cols {
13403            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
13404        }
13405        if row0 >= asheet.nrows as usize {
13406            asheet.ensure_row_capacity(row0 + 1);
13407        }
13408        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
13409            return;
13410        };
13411        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
13412            return;
13413        };
13414        let delta = ch.computed_overlay.set_scalar(in_off, ov);
13415        ch.computed_overlay.set_format(in_off, format);
13416        self.adjust_computed_overlay_bytes(delta);
13417        if let Some(cap) = self.config.max_overlay_memory_bytes
13418            && self.computed_overlay_bytes_estimate > cap
13419        {
13420            self.disable_computed_overlay_mirroring_due_to_budget(cap);
13421        }
13422    }
13423
13424    fn write_computed_overlay_value_0based(
13425        &mut self,
13426        sheet: &str,
13427        row0: u32,
13428        col0: u32,
13429        value: OverlayValue,
13430    ) {
13431        if !(self.config.arrow_storage_enabled
13432            && self.config.delta_overlay_enabled
13433            && self.config.write_formula_overlay_enabled)
13434        {
13435            return;
13436        }
13437        if self.computed_overlay_mirroring_disabled {
13438            return;
13439        }
13440
13441        self.ensure_arrow_sheet(sheet);
13442
13443        let row0 = row0 as usize;
13444        let col0 = col0 as usize;
13445        let asheet = self
13446            .arrow_sheets
13447            .sheet_mut(sheet)
13448            .expect("ArrowSheet must exist");
13449
13450        let cur_cols = asheet.columns.len();
13451        if col0 >= cur_cols {
13452            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
13453        }
13454
13455        if row0 >= asheet.nrows as usize {
13456            if asheet.columns.is_empty() {
13457                asheet.insert_columns(0, 1);
13458            }
13459            asheet.ensure_row_capacity(row0 + 1);
13460        }
13461
13462        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
13463            return;
13464        };
13465        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
13466            return;
13467        };
13468
13469        let delta = ch.computed_overlay.set_scalar(in_off, value);
13470        self.adjust_computed_overlay_bytes(delta);
13471
13472        if let Some(cap) = self.config.max_overlay_memory_bytes
13473            && self.computed_overlay_bytes_estimate > cap
13474        {
13475            self.disable_computed_overlay_mirroring_due_to_budget(cap);
13476        }
13477    }
13478
13479    pub(crate) fn plan_computed_write_coalescing(
13480        &self,
13481        buffer: &ComputedWriteBuffer,
13482    ) -> ComputedWriteCoalescingPlan {
13483        self.plan_computed_write_coalescing_from_writes(
13484            buffer.writes().iter().cloned(),
13485            buffer.formats_present,
13486        )
13487    }
13488
13489    fn plan_owned_computed_write_coalescing(
13490        &self,
13491        writes: Vec<ComputedWrite>,
13492        formats_present: bool,
13493    ) -> ComputedWriteCoalescingPlan {
13494        self.plan_computed_write_coalescing_from_writes(writes, formats_present)
13495    }
13496
13497    fn plan_computed_write_coalescing_from_writes(
13498        &self,
13499        writes: impl IntoIterator<Item = ComputedWrite>,
13500        formats_present: bool,
13501    ) -> ComputedWriteCoalescingPlan {
13502        let mut groups: BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>> =
13503            BTreeMap::new();
13504        let mut input_cells = 0usize;
13505        // One sheet lookup per sheet run of writes, not per cell.
13506        let mut located: Option<(SheetId, Option<&crate::arrow_store::ArrowSheet>)> = None;
13507
13508        for write in writes {
13509            match write {
13510                ComputedWrite::Cell {
13511                    seq,
13512                    sheet_id,
13513                    row0,
13514                    col0,
13515                    value,
13516                    format_id,
13517                } => {
13518                    input_cells = input_cells.saturating_add(1);
13519                    let sheet = match located {
13520                        Some((id, sheet)) if id == sheet_id => sheet,
13521                        _ => {
13522                            let sheet = self.arrow_sheets.sheet(self.graph.sheet_name(sheet_id));
13523                            located = Some((sheet_id, sheet));
13524                            sheet
13525                        }
13526                    };
13527                    let (chunk_idx, chunk_start_row0, row_in_chunk) = match sheet {
13528                        Some(sheet) => {
13529                            Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize)
13530                        }
13531                        None => Self::locate_row_in_empty_sheet_for_computed_write_plan(
13532                            row0 as usize,
13533                            32 * 1024,
13534                        ),
13535                    };
13536                    groups
13537                        .entry(ComputedWriteChunkKey {
13538                            sheet_id,
13539                            col0,
13540                            chunk_idx,
13541                            chunk_start_row0,
13542                        })
13543                        .or_default()
13544                        .push(ComputedWriteChunkEntryPlan {
13545                            row_in_chunk,
13546                            seq,
13547                            value,
13548                            format_id,
13549                        });
13550                }
13551                ComputedWrite::Run {
13552                    seq,
13553                    sheet_id,
13554                    row0,
13555                    col0,
13556                    entries,
13557                } => {
13558                    input_cells = input_cells.saturating_add(entries.len());
13559                    let sheet = match located {
13560                        Some((id, sheet)) if id == sheet_id => sheet,
13561                        _ => {
13562                            let sheet = self.arrow_sheets.sheet(self.graph.sheet_name(sheet_id));
13563                            located = Some((sheet_id, sheet));
13564                            sheet
13565                        }
13566                    };
13567                    // Rows are located one by one (a binary search, no sheet
13568                    // lookup); the group map is touched once per chunk
13569                    // segment of the run.
13570                    let mut segment: Vec<ComputedWriteChunkEntryPlan> = Vec::new();
13571                    let mut segment_key: Option<ComputedWriteChunkKey> = None;
13572                    for (k, (value, format_id)) in entries.into_iter().enumerate() {
13573                        let row = row0.saturating_add(k as u32) as usize;
13574                        let (chunk_idx, chunk_start_row0, row_in_chunk) = match sheet {
13575                            Some(sheet) => {
13576                                Self::locate_row_in_sheet_for_computed_write_plan(sheet, row)
13577                            }
13578                            None => Self::locate_row_in_empty_sheet_for_computed_write_plan(
13579                                row,
13580                                32 * 1024,
13581                            ),
13582                        };
13583                        let key = ComputedWriteChunkKey {
13584                            sheet_id,
13585                            col0,
13586                            chunk_idx,
13587                            chunk_start_row0,
13588                        };
13589                        if segment_key != Some(key)
13590                            && let Some(done) = segment_key.replace(key)
13591                        {
13592                            groups.entry(done).or_default().append(&mut segment);
13593                        }
13594                        segment.push(ComputedWriteChunkEntryPlan {
13595                            row_in_chunk,
13596                            seq,
13597                            value,
13598                            format_id,
13599                        });
13600                    }
13601                    if let Some(done) = segment_key {
13602                        groups.entry(done).or_default().append(&mut segment);
13603                    }
13604                }
13605                ComputedWrite::Rect {
13606                    seq,
13607                    sheet_id,
13608                    sr0,
13609                    sc0,
13610                    values,
13611                } => {
13612                    for (r_off, row) in values.into_iter().enumerate() {
13613                        for (c_off, value) in row.into_iter().enumerate() {
13614                            input_cells = input_cells.saturating_add(1);
13615                            self.push_computed_write_plan_entry(
13616                                &mut groups,
13617                                seq,
13618                                sheet_id,
13619                                sr0.saturating_add(r_off as u32),
13620                                sc0.saturating_add(c_off as u32),
13621                                value,
13622                                None,
13623                            );
13624                        }
13625                    }
13626                }
13627            }
13628        }
13629
13630        let mut plan = ComputedWriteCoalescingPlan {
13631            chunks: Vec::with_capacity(groups.len()),
13632            input_cells,
13633            coalesced_cells: 0,
13634            overwritten_cells: 0,
13635        };
13636        for (key, entries) in groups {
13637            let computed_lane_has_formats =
13638                self.computed_overlay_chunk_has_formats(key.sheet_id, key.col0, key.chunk_idx);
13639            let (chunk_plan, overwritten) = ComputedWriteChunkPlan::from_group(
13640                key,
13641                entries,
13642                formats_present,
13643                computed_lane_has_formats,
13644            );
13645            #[cfg(test)]
13646            if matches!(
13647                &chunk_plan.format_effect,
13648                ComputedWriteChunkFormatEffect::SetExplicit(_)
13649            ) {
13650                self.computed_format_vector_allocations_for_test
13651                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
13652            }
13653            plan.coalesced_cells = plan
13654                .coalesced_cells
13655                .saturating_add(chunk_plan.entries.len());
13656            plan.overwritten_cells = plan.overwritten_cells.saturating_add(overwritten);
13657            plan.chunks.push(chunk_plan);
13658        }
13659        debug_assert_eq!(
13660            plan.input_cells,
13661            plan.coalesced_cells.saturating_add(plan.overwritten_cells)
13662        );
13663        plan
13664    }
13665
13666    fn push_computed_write_plan_entry(
13667        &self,
13668        groups: &mut BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>>,
13669        seq: u64,
13670        sheet_id: SheetId,
13671        row0: u32,
13672        col0: u32,
13673        value: OverlayValue,
13674        format_id: Option<crate::format::FormatId>,
13675    ) {
13676        let (chunk_idx, chunk_start_row0, row_in_chunk) =
13677            self.locate_computed_write_chunk(sheet_id, row0);
13678        let key = ComputedWriteChunkKey {
13679            sheet_id,
13680            col0,
13681            chunk_idx,
13682            chunk_start_row0,
13683        };
13684        groups
13685            .entry(key)
13686            .or_default()
13687            .push(ComputedWriteChunkEntryPlan {
13688                row_in_chunk,
13689                seq,
13690                value,
13691                format_id,
13692            });
13693    }
13694
13695    fn computed_overlay_chunk_has_formats(
13696        &self,
13697        sheet_id: SheetId,
13698        col0: u32,
13699        chunk_idx: usize,
13700    ) -> bool {
13701        let sheet_name = self.graph.sheet_name(sheet_id);
13702        self.arrow_sheets
13703            .sheet(sheet_name)
13704            .and_then(|sheet| sheet.columns.get(col0 as usize))
13705            .and_then(|column| column.chunk(chunk_idx))
13706            .is_some_and(|chunk| chunk.computed_overlay.has_formats())
13707    }
13708
13709    fn locate_computed_write_chunk(&self, sheet_id: SheetId, row0: u32) -> (usize, u32, usize) {
13710        let sheet_name = self.graph.sheet_name(sheet_id);
13711        if let Some(sheet) = self.arrow_sheets.sheet(sheet_name) {
13712            return Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize);
13713        }
13714        Self::locate_row_in_empty_sheet_for_computed_write_plan(row0 as usize, 32 * 1024)
13715    }
13716
13717    fn locate_row_in_sheet_for_computed_write_plan(
13718        sheet: &crate::arrow_store::ArrowSheet,
13719        row0: usize,
13720    ) -> (usize, u32, usize) {
13721        if row0 < sheet.nrows as usize
13722            && let Some((chunk_idx, row_in_chunk)) = sheet.chunk_of_row(row0)
13723        {
13724            let chunk_start = sheet.chunk_starts.get(chunk_idx).copied().unwrap_or(0);
13725            return (chunk_idx, chunk_start as u32, row_in_chunk);
13726        }
13727
13728        let chunk_rows = sheet.chunk_rows.max(1);
13729        if sheet.chunk_starts.is_empty() {
13730            return Self::locate_row_in_empty_sheet_for_computed_write_plan(row0, chunk_rows);
13731        }
13732
13733        let mut chunk_idx = sheet.chunk_starts.len().saturating_sub(1);
13734        let mut chunk_start = sheet.chunk_starts[chunk_idx];
13735        while chunk_start.saturating_add(chunk_rows) <= row0 {
13736            chunk_idx = chunk_idx.saturating_add(1);
13737            chunk_start = chunk_start.saturating_add(chunk_rows);
13738        }
13739        (
13740            chunk_idx,
13741            chunk_start as u32,
13742            row0.saturating_sub(chunk_start),
13743        )
13744    }
13745
13746    fn locate_row_in_empty_sheet_for_computed_write_plan(
13747        row0: usize,
13748        chunk_rows: usize,
13749    ) -> (usize, u32, usize) {
13750        let chunk_rows = chunk_rows.max(1);
13751        let chunk_idx = row0 / chunk_rows;
13752        let chunk_start = chunk_idx.saturating_mul(chunk_rows);
13753        (
13754            chunk_idx,
13755            chunk_start as u32,
13756            row0.saturating_sub(chunk_start),
13757        )
13758    }
13759
13760    #[cfg(test)]
13761    pub(crate) fn debug_plan_computed_write_coalescing(
13762        &self,
13763        buffer: &ComputedWriteBuffer,
13764    ) -> ComputedWriteCoalescingPlan {
13765        self.plan_computed_write_coalescing(buffer)
13766    }
13767
13768    pub(crate) fn flush_computed_write_buffer(
13769        &mut self,
13770        buffer: &mut ComputedWriteBuffer,
13771    ) -> Result<(), ExcelError> {
13772        if buffer.is_empty() {
13773            return Ok(());
13774        }
13775
13776        // Keep ownership of all pending writes until the final request
13777        // checkpoint and bounded commit-window preflight succeed so failures
13778        // remain retry safe. The synchronization and flush that follow are
13779        // infallible mutations with no cancellation point.
13780        self.resource_checkpoint(0)?;
13781        let commit_started = self.preflight_evaluation_commit_window(buffer.len())?;
13782        let (writes, formats_present) = buffer.take_writes();
13783        let mut claims = std::mem::take(&mut buffer.read_claims);
13784        let plan = self.plan_owned_computed_write_coalescing(writes, formats_present);
13785        if !claims.is_empty() {
13786            for chunk in &plan.chunks {
13787                for entry in &chunk.entries {
13788                    let key = (
13789                        chunk.sheet_id,
13790                        chunk
13791                            .chunk_start_row0
13792                            .saturating_add(entry.row_in_chunk as u32),
13793                        chunk.col0,
13794                    );
13795                    if let Some(claim) = claims.get_mut(&key) {
13796                        claim.winning = claim.seq == entry.seq;
13797                    }
13798                }
13799            }
13800        }
13801        self.flush_computed_write_plan(plan);
13802        for claim in claims.into_values() {
13803            if claim.winning {
13804                self.freshness_record_installed(&claim.result, true);
13805            }
13806        }
13807        self.observe_evaluation_commit_window(commit_started);
13808
13809        Ok(())
13810    }
13811
13812    fn flush_computed_write_plan(&mut self, plan: ComputedWriteCoalescingPlan) {
13813        for chunk in plan.chunks {
13814            self.flush_computed_write_chunk_plan(chunk);
13815        }
13816    }
13817
13818    fn flush_computed_write_chunk_plan(&mut self, chunk: ComputedWriteChunkPlan) {
13819        match &chunk.shape {
13820            ComputedWriteChunkPlanShape::Point => {
13821                self.flush_computed_write_chunk_plan_as_points(chunk);
13822            }
13823            ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
13824                self.flush_computed_write_chunk_plan_as_sparse_fragment_or_points(chunk);
13825            }
13826            ComputedWriteChunkPlanShape::DenseRange { .. } => {
13827                self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
13828            }
13829            ComputedWriteChunkPlanShape::RunRange { len, runs, .. } => {
13830                if Self::should_emit_computed_run_fragment(*len, *runs) {
13831                    self.flush_computed_write_chunk_plan_as_run_fragment(chunk);
13832                } else {
13833                    self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
13834                }
13835            }
13836        }
13837    }
13838
13839    #[inline]
13840    fn should_emit_computed_run_fragment(len: usize, runs: usize) -> bool {
13841        runs <= len / 2
13842    }
13843
13844    fn flush_computed_write_chunk_plan_as_points(&mut self, chunk: ComputedWriteChunkPlan) {
13845        let sheet_name = self.graph.sheet_name(chunk.sheet_id).to_string();
13846        for entry in chunk.entries {
13847            let row0 = chunk
13848                .chunk_start_row0
13849                .saturating_add(entry.row_in_chunk as u32);
13850            self.write_computed_overlay_value_0based(&sheet_name, row0, chunk.col0, entry.value);
13851        }
13852        self.apply_computed_overlay_format_effect(
13853            chunk.sheet_id,
13854            chunk.col0,
13855            chunk.chunk_idx,
13856            chunk.format_effect,
13857        );
13858    }
13859
13860    fn flush_computed_write_chunk_plan_as_sparse_fragment_or_points(
13861        &mut self,
13862        chunk: ComputedWriteChunkPlan,
13863    ) {
13864        let point_estimate = Self::computed_write_chunk_plan_point_estimate(&chunk);
13865        let sheet_id = chunk.sheet_id;
13866        let col0 = chunk.col0;
13867        let chunk_idx = chunk.chunk_idx;
13868        let chunk_start_row0 = chunk.chunk_start_row0;
13869        let format_effect = chunk.format_effect;
13870        let items: Vec<(usize, OverlayValue)> = chunk
13871            .entries
13872            .into_iter()
13873            .map(|entry| (entry.row_in_chunk, entry.value))
13874            .collect();
13875        match OverlayFragment::sparse_offsets_if_estimated_smaller_than_points(
13876            items,
13877            point_estimate,
13878        ) {
13879            Some(Ok(fragment)) => {
13880                self.apply_computed_overlay_fragment(sheet_id, col0, chunk_idx, fragment);
13881            }
13882            Some(Err(cells)) => {
13883                self.flush_computed_overlay_cells_as_points(
13884                    sheet_id,
13885                    col0,
13886                    chunk_start_row0,
13887                    cells,
13888                );
13889            }
13890            None => {}
13891        }
13892        self.apply_computed_overlay_format_effect(sheet_id, col0, chunk_idx, format_effect);
13893    }
13894
13895    #[inline]
13896    fn computed_write_chunk_plan_point_estimate(chunk: &ComputedWriteChunkPlan) -> usize {
13897        chunk
13898            .entries
13899            .iter()
13900            .map(|entry| ComputedWriteBuffer::estimate_value_bytes(&entry.value))
13901            .fold(0usize, usize::saturating_add)
13902    }
13903
13904    fn flush_computed_overlay_cells_as_points(
13905        &mut self,
13906        sheet_id: SheetId,
13907        col0: u32,
13908        chunk_start_row0: u32,
13909        cells: Vec<(usize, OverlayValue)>,
13910    ) {
13911        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
13912        for (row_in_chunk, value) in cells {
13913            let row0 = chunk_start_row0.saturating_add(row_in_chunk as u32);
13914            self.write_computed_overlay_value_0based(&sheet_name, row0, col0, value);
13915        }
13916    }
13917
13918    fn flush_computed_write_chunk_plan_as_dense_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
13919        if chunk.entries.is_empty() {
13920            return;
13921        }
13922        let start = chunk.entries[0].row_in_chunk;
13923        let values: Vec<OverlayValue> =
13924            chunk.entries.into_iter().map(|entry| entry.value).collect();
13925        if let Some(fragment) = OverlayFragment::dense_range(start, values) {
13926            self.apply_computed_overlay_fragment(
13927                chunk.sheet_id,
13928                chunk.col0,
13929                chunk.chunk_idx,
13930                fragment,
13931            );
13932        }
13933        self.apply_computed_overlay_format_effect(
13934            chunk.sheet_id,
13935            chunk.col0,
13936            chunk.chunk_idx,
13937            chunk.format_effect,
13938        );
13939    }
13940
13941    fn flush_computed_write_chunk_plan_as_run_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
13942        if chunk.entries.is_empty() {
13943            return;
13944        }
13945        let start = chunk.entries[0].row_in_chunk;
13946        let values: Vec<OverlayValue> =
13947            chunk.entries.into_iter().map(|entry| entry.value).collect();
13948        if let Some(fragment) = OverlayFragment::run_range(start, values) {
13949            self.apply_computed_overlay_fragment(
13950                chunk.sheet_id,
13951                chunk.col0,
13952                chunk.chunk_idx,
13953                fragment,
13954            );
13955        }
13956        self.apply_computed_overlay_format_effect(
13957            chunk.sheet_id,
13958            chunk.col0,
13959            chunk.chunk_idx,
13960            chunk.format_effect,
13961        );
13962    }
13963
13964    fn apply_computed_overlay_format_effect(
13965        &mut self,
13966        sheet_id: SheetId,
13967        col0: u32,
13968        chunk_idx: usize,
13969        effect: ComputedWriteChunkFormatEffect,
13970    ) {
13971        if !(self.config.arrow_storage_enabled
13972            && self.config.delta_overlay_enabled
13973            && self.config.write_formula_overlay_enabled)
13974            || self.computed_overlay_mirroring_disabled
13975        {
13976            return;
13977        }
13978
13979        let sheet_name = self.graph.sheet_name(sheet_id);
13980        let Some(sheet) = self.arrow_sheets.sheet_mut(sheet_name) else {
13981            return;
13982        };
13983        let Some(chunk) = sheet
13984            .columns
13985            .get_mut(col0 as usize)
13986            .and_then(|column| column.chunk_mut(chunk_idx))
13987        else {
13988            return;
13989        };
13990        match effect {
13991            ComputedWriteChunkFormatEffect::NoFormatWork => {}
13992            ComputedWriteChunkFormatEffect::ClearStale(ComputedWriteFormatClear::Range {
13993                start,
13994                end,
13995            }) => {
13996                #[cfg(test)]
13997                {
13998                    self.computed_overlay_stale_clear_range_effects_for_test = self
13999                        .computed_overlay_stale_clear_range_effects_for_test
14000                        .saturating_add(1);
14001                }
14002                chunk.computed_overlay.clear_format_range(start, end);
14003            }
14004            ComputedWriteChunkFormatEffect::ClearStale(ComputedWriteFormatClear::Offsets(
14005                offsets,
14006            )) => {
14007                #[cfg(test)]
14008                {
14009                    self.computed_overlay_stale_clear_offset_attempts_for_test = self
14010                        .computed_overlay_stale_clear_offset_attempts_for_test
14011                        .saturating_add(offsets.len() as u64);
14012                }
14013                chunk.computed_overlay.clear_format_offsets(&offsets);
14014            }
14015            ComputedWriteChunkFormatEffect::SetExplicit(formats) => {
14016                #[cfg(test)]
14017                {
14018                    self.computed_overlay_set_explicit_entry_operations_for_test = self
14019                        .computed_overlay_set_explicit_entry_operations_for_test
14020                        .saturating_add(formats.len() as u64);
14021                }
14022                for (row_in_chunk, format_id) in formats {
14023                    chunk.computed_overlay.set_format(row_in_chunk, format_id);
14024                }
14025            }
14026        }
14027    }
14028
14029    fn apply_computed_overlay_fragment(
14030        &mut self,
14031        sheet_id: SheetId,
14032        col0: u32,
14033        chunk_idx: usize,
14034        fragment: OverlayFragment,
14035    ) {
14036        if !(self.config.arrow_storage_enabled
14037            && self.config.delta_overlay_enabled
14038            && self.config.write_formula_overlay_enabled)
14039        {
14040            return;
14041        }
14042        if self.computed_overlay_mirroring_disabled {
14043            return;
14044        }
14045
14046        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
14047        self.ensure_arrow_sheet(&sheet_name);
14048
14049        let col0 = col0 as usize;
14050        let asheet = self
14051            .arrow_sheets
14052            .sheet_mut(&sheet_name)
14053            .expect("ArrowSheet must exist");
14054
14055        let cur_cols = asheet.columns.len();
14056        if col0 >= cur_cols {
14057            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
14058        }
14059
14060        let start_row0 = asheet
14061            .chunk_starts
14062            .get(chunk_idx)
14063            .copied()
14064            .unwrap_or_else(|| chunk_idx.saturating_mul(asheet.chunk_rows.max(1)));
14065        let required_rows =
14066            start_row0.saturating_add(fragment.max_covered_offset().saturating_add(1));
14067        if required_rows > asheet.nrows as usize {
14068            if asheet.columns.is_empty() {
14069                asheet.insert_columns(0, 1);
14070            }
14071            asheet.ensure_row_capacity(required_rows);
14072        }
14073
14074        let Some(ch) = asheet.ensure_column_chunk_mut(col0, chunk_idx) else {
14075            return;
14076        };
14077        let delta = ch.computed_overlay.apply_fragment(fragment);
14078        self.adjust_computed_overlay_bytes(delta);
14079
14080        if let Some(cap) = self.config.max_overlay_memory_bytes
14081            && self.computed_overlay_bytes_estimate > cap
14082        {
14083            self.disable_computed_overlay_mirroring_due_to_budget(cap);
14084        }
14085    }
14086
14087    #[inline]
14088    fn adjust_computed_overlay_bytes(&mut self, delta: isize) {
14089        if delta >= 0 {
14090            self.computed_overlay_bytes_estimate = self
14091                .computed_overlay_bytes_estimate
14092                .saturating_add(delta as usize);
14093        } else {
14094            self.computed_overlay_bytes_estimate = self
14095                .computed_overlay_bytes_estimate
14096                .saturating_sub((-delta) as usize);
14097        }
14098    }
14099
14100    fn clear_all_computed_overlays(&mut self) {
14101        let mut freed_total = 0usize;
14102        for sh in self.arrow_sheets.sheets.iter_mut() {
14103            for col in sh.columns.iter_mut() {
14104                for ch in col.chunks.iter_mut() {
14105                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
14106                }
14107                for ch in col.sparse_chunks.values_mut() {
14108                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
14109                }
14110            }
14111        }
14112        self.computed_overlay_bytes_estimate = self
14113            .computed_overlay_bytes_estimate
14114            .saturating_sub(freed_total);
14115    }
14116
14117    fn disable_computed_overlay_mirroring_due_to_budget(&mut self, _cap: usize) {
14118        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
14119        // Handle budget pressure by compacting computed overlays into base lanes.
14120        self.compact_all_computed_overlays();
14121    }
14122
14123    /// Fold all computed overlay entries across all sheets into their base arrays.
14124    /// This preserves data while freeing overlay memory, allowing mirroring to continue.
14125    fn compact_all_computed_overlays(&mut self) {
14126        let mut freed_total = 0usize;
14127        for sheet in self.arrow_sheets.sheets.iter_mut() {
14128            for col_idx in 0..sheet.columns.len() {
14129                // Dense chunks
14130                let num_dense = sheet.columns[col_idx].chunks.len();
14131                for ch_idx in 0..num_dense {
14132                    freed_total += sheet.compact_computed_overlay_chunk(col_idx, ch_idx);
14133                }
14134                // Sparse chunks
14135                let sparse_keys: Vec<usize> = sheet.columns[col_idx]
14136                    .sparse_chunks
14137                    .keys()
14138                    .copied()
14139                    .collect();
14140                for ch_idx in sparse_keys {
14141                    freed_total += sheet.compact_computed_overlay_sparse_chunk(col_idx, ch_idx);
14142                }
14143            }
14144        }
14145        self.computed_overlay_bytes_estimate = self
14146            .computed_overlay_bytes_estimate
14147            .saturating_sub(freed_total);
14148        self.overlay_compactions = self.overlay_compactions.saturating_add(1);
14149    }
14150
14151    fn mirror_vertex_value_to_overlay(&mut self, vertex_id: VertexId, value: &LiteralValue) {
14152        let _ = self.record_vertex_value_to_overlay(vertex_id, value, None);
14153    }
14154
14155    fn record_vertex_value_to_overlay(
14156        &mut self,
14157        vertex_id: VertexId,
14158        value: &LiteralValue,
14159        computed_writes: Option<&mut ComputedWriteBuffer>,
14160    ) -> Result<OverlayWriteDisposition, ExcelError> {
14161        if !(self.config.arrow_storage_enabled
14162            && self.config.delta_overlay_enabled
14163            && self.config.write_formula_overlay_enabled)
14164        {
14165            return Ok(OverlayWriteDisposition::Skipped);
14166        }
14167        if self.computed_overlay_mirroring_disabled {
14168            return Ok(OverlayWriteDisposition::Skipped);
14169        }
14170        if !matches!(
14171            self.graph.get_vertex_kind(vertex_id),
14172            VertexKind::FormulaScalar | VertexKind::FormulaArray
14173        ) {
14174            return Ok(OverlayWriteDisposition::Skipped);
14175        }
14176        let Some(cell) = self.graph.get_cell_ref(vertex_id) else {
14177            return Ok(OverlayWriteDisposition::Skipped);
14178        };
14179        let Some(buffer) = computed_writes else {
14180            // Unbuffered: one sheet lookup for the value and its format.
14181            let format_id = self.derived_formats.get(&cell);
14182            self.write_computed_cell_0based(cell, value, format_id);
14183            return Ok(OverlayWriteDisposition::Installed);
14184        };
14185        let date_system = self
14186            .arrow_sheet_by_id(cell.sheet_id)
14187            .map(|sheet| sheet.date_system)
14188            .unwrap_or(self.config.date_system);
14189        let ov = Self::literal_to_overlay_value(value, date_system);
14190        {
14191            let format_id = self.derived_formats.get(&cell);
14192            buffer.push_cell_with_format(
14193                cell.sheet_id,
14194                cell.coord.row(),
14195                cell.coord.col(),
14196                ov,
14197                format_id,
14198            );
14199            if self.should_flush_computed_write_buffer(buffer) {
14200                self.flush_computed_write_buffer(buffer)?;
14201                return Ok(OverlayWriteDisposition::Installed);
14202            }
14203        }
14204        Ok(OverlayWriteDisposition::Buffered)
14205    }
14206
14207    #[inline]
14208    fn should_flush_computed_write_buffer(&self, buffer: &ComputedWriteBuffer) -> bool {
14209        self.config.max_overlay_memory_bytes.is_some_and(|cap| {
14210            if cap == 0 {
14211                return false;
14212            }
14213            self.computed_overlay_bytes_estimate
14214                .saturating_add(buffer.estimated_bytes())
14215                > cap
14216        })
14217    }
14218
14219    /// Estimated memory usage for computed overlays (formula/spill mirroring).
14220    pub fn overlay_memory_usage(&self) -> usize {
14221        self.computed_overlay_bytes_estimate
14222    }
14223
14224    #[cfg(test)]
14225    pub(crate) fn debug_overlay_compactions(&self) -> u64 {
14226        self.overlay_compactions
14227    }
14228
14229    #[cfg(test)]
14230    pub(crate) fn debug_recompute_computed_overlay_bytes(&mut self) -> usize {
14231        let mut total = 0usize;
14232        for sheet in &self.arrow_sheets.sheets {
14233            for column in &sheet.columns {
14234                for chunk in &column.chunks {
14235                    total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
14236                }
14237                for chunk in column.sparse_chunks.values() {
14238                    total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
14239                }
14240            }
14241        }
14242        self.computed_overlay_bytes_estimate = total;
14243        total
14244    }
14245
14246    fn resolve_sheet_locator_for_write(
14247        &mut self,
14248        loc: formualizer_common::SheetLocator<'_>,
14249        current_sheet: &str,
14250    ) -> Result<SheetId, ExcelError> {
14251        Ok(match loc {
14252            formualizer_common::SheetLocator::Id(id) => id,
14253            formualizer_common::SheetLocator::Name(name) => self.graph.sheet_id_mut(name.as_ref()),
14254            formualizer_common::SheetLocator::Current => self.graph.sheet_id_mut(current_sheet),
14255        })
14256    }
14257
14258    fn resolve_sheet_locator_for_read(
14259        &self,
14260        loc: formualizer_common::SheetLocator<'_>,
14261        current_sheet: &str,
14262    ) -> Result<SheetId, ExcelError> {
14263        match loc {
14264            formualizer_common::SheetLocator::Id(id) => Ok(id),
14265            formualizer_common::SheetLocator::Name(name) => self
14266                .graph
14267                .sheet_id(name.as_ref())
14268                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
14269            formualizer_common::SheetLocator::Current => self
14270                .graph
14271                .sheet_id(current_sheet)
14272                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
14273        }
14274    }
14275
14276    /// Set a cell value
14277    pub fn set_cell_value(
14278        &mut self,
14279        sheet: &str,
14280        row: u32,
14281        col: u32,
14282        value: LiteralValue,
14283    ) -> Result<(), ExcelError> {
14284        self.ensure_not_poisoned()?;
14285        self.observe_function_semantic_epoch()?;
14286        let sheet_existed = self.graph.sheet_id(sheet).is_some();
14287        let sheet_id = self.graph.sheet_id_mut(sheet);
14288        let cell_ref = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14289        let replaced = self
14290            .graph
14291            .get_vertex_id_for_address(&cell_ref)
14292            .filter(|&vertex| {
14293                matches!(
14294                    self.graph.get_vertex_kind(vertex),
14295                    VertexKind::FormulaScalar | VertexKind::FormulaArray
14296                )
14297            });
14298        let replaced_formula = replaced.is_some();
14299        // A value replacing a spilled anchor takes its spill with it (no
14300        // evaluation will revisit the anchor); anchors that spill blocked
14301        // wake through the cleared region.
14302        if let Some(anchor) = replaced
14303            && self.graph.spill_registry_has_anchor(anchor)
14304        {
14305            self.clear_spill_projection_and_mirror(anchor, None);
14306        }
14307        self.graph.set_cell_value(sheet, row, col, value.clone())?;
14308        if value != LiteralValue::Empty {
14309            for table in self.graph.empty_tables_at(cell_ref) {
14310                self.graph.resize_table(&table.name, table.range)?;
14311                self.mark_topology_edited();
14312            }
14313        }
14314        self.clear_cell_format_state(sheet, cell_ref);
14315        self.record_changed_cell(sheet, row, col);
14316        if !sheet_existed || replaced_formula {
14317            self.mark_topology_edited();
14318        }
14319        // Mirror into Arrow overlay when enabled
14320        self.mirror_value_to_overlay(sheet, row, col, &value);
14321        // Advance snapshot to reflect external mutation.
14322        self.mark_data_edited();
14323        Ok(())
14324    }
14325
14326    /// Record a single-cell change: invalidates pending spills blocked on it.
14327    fn record_changed_cell(&mut self, sheet: &str, row: u32, col: u32) {
14328        let sheet_id = self.graph.sheet_id_mut(sheet);
14329        self.record_structural_change(StructuralScope::Cell {
14330            sheet: sheet_id,
14331            row: row.saturating_sub(1),
14332            col: col.saturating_sub(1),
14333        });
14334    }
14335
14336    fn record_inverse_change_for_event(&mut self, event: &ChangeEvent) {
14337        if let ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } = event {
14338            // Replay already restored any spill snapshot's ownership. This
14339            // notification is not a new literal write into that projection.
14340            self.record_structural_change(StructuralScope::Region(Region::point(
14341                addr.sheet_id,
14342                addr.coord.row(),
14343                addr.coord.col(),
14344            )));
14345        } else {
14346            self.record_change_for_event(event);
14347        }
14348    }
14349
14350    fn record_change_for_event(&mut self, event: &ChangeEvent) {
14351        match event {
14352            ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
14353                self.record_structural_change(StructuralScope::Cell {
14354                    sheet: addr.sheet_id,
14355                    row: addr.coord.row(),
14356                    col: addr.coord.col(),
14357                });
14358            }
14359            ChangeEvent::SpillCommitted { new, .. } => {
14360                if let Some(scope) = Self::structural_scope_from_cells(&new.target_cells) {
14361                    self.record_structural_change(scope);
14362                }
14363            }
14364            ChangeEvent::SpillCleared { anchor, old } => {
14365                if self.graph.is_live_formula_vertex(*anchor) {
14366                    self.graph.mark_dirty_many(&[*anchor]);
14367                }
14368                if let Some(scope) = Self::structural_scope_from_cells(&old.target_cells) {
14369                    self.record_structural_change(scope);
14370                }
14371            }
14372            ChangeEvent::TableResized { .. }
14373            | ChangeEvent::DefineName { .. }
14374            | ChangeEvent::UpdateName { .. }
14375            | ChangeEvent::DeleteName { .. }
14376            | ChangeEvent::NamedRangeAdjusted { .. } => {
14377                // Direct name events are preflighted by the logged-name APIs.
14378                // Structural entry points preflight spans before emitting a
14379                // NamedRangeAdjusted event. Epoch changes only rebuild caches.
14380                self.record_structural_change(StructuralScope::AllSheets);
14381            }
14382            ChangeEvent::VertexMoved { id, .. } | ChangeEvent::FormulaAdjusted { id, .. } => {
14383                // Structural replay restores cached values in the old frame;
14384                // moved foreign formulas must recompute there as well as spill
14385                // anchors. The forward structural entry point also dirties them.
14386                if self.graph.is_live_formula_vertex(*id) {
14387                    self.graph.mark_dirty_many(&[*id]);
14388                }
14389            }
14390            ChangeEvent::SetRowVisibility { sheet_id, row0, .. } => {
14391                self.record_structural_change(StructuralScope::Region(Region::whole_row(
14392                    *sheet_id, *row0,
14393                )));
14394            }
14395            ChangeEvent::AddVertex { .. }
14396            | ChangeEvent::RemoveVertex { .. }
14397            | ChangeEvent::EdgeAdded { .. }
14398            | ChangeEvent::EdgeRemoved { .. }
14399            | ChangeEvent::CompoundStart { .. }
14400            | ChangeEvent::CompoundEnd { .. }
14401            | ChangeEvent::StagedFormulaCellChanged { .. } => {}
14402        }
14403    }
14404
14405    fn record_structural_change(&mut self, scope: StructuralScope) {
14406        if let StructuralScope::Cell { sheet, row, col } = scope {
14407            self.wake_spill_owner_of_entered_cell(sheet, row, col);
14408        }
14409        self.wake_spill_blocker_waiters(scope);
14410        self.invalidate_pending_spills(scope);
14411    }
14412
14413    /// Wake anchors waiting on a blocking cell inside `scope`, with their
14414    /// dependents: the anchor's value changes when it spills. A woken anchor
14415    /// registers again if it is still blocked.
14416    fn wake_spill_blocker_waiters(&mut self, scope: StructuralScope) {
14417        if self.spill_blocker_waiters.is_empty() {
14418            return;
14419        }
14420        let woken = self.spill_blocker_waiters.take_affected(scope);
14421        let woken: Vec<VertexId> = woken
14422            .into_iter()
14423            .filter(|&vertex| {
14424                self.graph.vertex_exists(vertex)
14425                    && matches!(
14426                        self.graph.get_vertex_kind(vertex),
14427                        VertexKind::FormulaScalar | VertexKind::FormulaArray
14428                    )
14429            })
14430            .collect();
14431        if !woken.is_empty() {
14432            self.graph.mark_dirty_many(&woken);
14433        }
14434    }
14435
14436    /// A user edit into a live spill wakes its anchor. Literal writes release
14437    /// projection ownership before any later shrink or clear.
14438    fn wake_spill_owner_of_entered_cell(&mut self, sheet: SheetId, row: u32, col: u32) {
14439        let cell = self.graph.make_cell_ref_internal(sheet, row, col);
14440        if let Some(owner) = self.graph.note_spill_child_edit(cell) {
14441            self.graph.mark_dirty_many(&[owner]);
14442        }
14443    }
14444
14445    fn structural_scope_from_cells(cells: &[CellRef]) -> Option<StructuralScope> {
14446        let first = cells.first()?;
14447        let sheet_id = first.sheet_id;
14448        if cells.iter().any(|cell| cell.sheet_id != sheet_id) {
14449            return Some(StructuralScope::OpaqueGlobal);
14450        }
14451        let mut row_start = first.coord.row();
14452        let mut row_end = row_start;
14453        let mut col_start = first.coord.col();
14454        let mut col_end = col_start;
14455        for cell in cells.iter().skip(1) {
14456            row_start = row_start.min(cell.coord.row());
14457            row_end = row_end.max(cell.coord.row());
14458            col_start = col_start.min(cell.coord.col());
14459            col_end = col_end.max(cell.coord.col());
14460        }
14461        Some(StructuralScope::Region(Region::rect(
14462            sheet_id, row_start, row_end, col_start, col_end,
14463        )))
14464    }
14465
14466    pub fn set_cell_value_ref(
14467        &mut self,
14468        cell: formualizer_common::SheetCellRef<'_>,
14469        current_sheet: &str,
14470        value: LiteralValue,
14471    ) -> Result<(), ExcelError> {
14472        self.ensure_not_poisoned()?;
14473        let owned = cell.into_owned();
14474        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
14475        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
14476        self.set_cell_value(
14477            &sheet_name,
14478            owned.coord.row() + 1,
14479            owned.coord.col() + 1,
14480            value,
14481        )
14482    }
14483
14484    pub fn set_cell_formula_ref(
14485        &mut self,
14486        cell: formualizer_common::SheetCellRef<'_>,
14487        current_sheet: &str,
14488        ast: ASTNode,
14489    ) -> Result<(), ExcelError> {
14490        self.ensure_not_poisoned()?;
14491        let owned = cell.into_owned();
14492        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
14493        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
14494        self.set_cell_formula(
14495            &sheet_name,
14496            owned.coord.row() + 1,
14497            owned.coord.col() + 1,
14498            ast,
14499        )
14500    }
14501
14502    pub fn get_cell_value_ref(
14503        &self,
14504        cell: formualizer_common::SheetCellRef<'_>,
14505        current_sheet: &str,
14506    ) -> Result<Option<LiteralValue>, ExcelError> {
14507        let owned = cell.into_owned();
14508        let sheet_id = self.resolve_sheet_locator_for_read(owned.sheet, current_sheet)?;
14509        let sheet_name = self.graph.sheet_name(sheet_id);
14510        Ok(self.get_cell_value(sheet_name, owned.coord.row() + 1, owned.coord.col() + 1))
14511    }
14512
14513    pub fn resolve_range_view_sheet_ref<'c>(
14514        &'c self,
14515        r: &formualizer_common::SheetRef<'_>,
14516        current_sheet: &str,
14517    ) -> Result<RangeView<'c>, ExcelError> {
14518        use formualizer_common::SheetLocator;
14519
14520        let sheet_to_opt_name = |loc: SheetLocator<'_>| -> Result<Option<String>, ExcelError> {
14521            match loc {
14522                SheetLocator::Current => Ok(None),
14523                SheetLocator::Name(name) => Ok(Some(name.as_ref().to_string())),
14524                SheetLocator::Id(id) => Ok(Some(self.graph.sheet_name(id).to_string())),
14525            }
14526        };
14527
14528        let rt = match r {
14529            formualizer_common::SheetRef::Cell(cell) => ReferenceType::Cell {
14530                sheet: sheet_to_opt_name(cell.sheet.clone())?,
14531                row: cell.coord.row() + 1,
14532                col: cell.coord.col() + 1,
14533                row_abs: cell.coord.row_abs(),
14534                col_abs: cell.coord.col_abs(),
14535            },
14536            formualizer_common::SheetRef::Range(range) => ReferenceType::Range {
14537                sheet: sheet_to_opt_name(range.sheet.clone())?,
14538                start_row: range.start_row.map(|b| b.index + 1),
14539                start_col: range.start_col.map(|b| b.index + 1),
14540                end_row: range.end_row.map(|b| b.index + 1),
14541                end_col: range.end_col.map(|b| b.index + 1),
14542                start_row_abs: range.start_row.map(|b| b.abs).unwrap_or(false),
14543                start_col_abs: range.start_col.map(|b| b.abs).unwrap_or(false),
14544                end_row_abs: range.end_row.map(|b| b.abs).unwrap_or(false),
14545                end_col_abs: range.end_col.map(|b| b.abs).unwrap_or(false),
14546            },
14547        };
14548
14549        crate::traits::EvaluationContext::resolve_range_view(self, &rt, current_sheet)
14550    }
14551
14552    /// Set a cell formula
14553    pub fn set_cell_formula(
14554        &mut self,
14555        sheet: &str,
14556        row: u32,
14557        col: u32,
14558        ast: ASTNode,
14559    ) -> Result<(), ExcelError> {
14560        self.ensure_not_poisoned()?;
14561        self.observe_function_semantic_epoch()?;
14562        let sheet_id = self.graph.sheet_id_mut(sheet);
14563        let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14564        let ingested = {
14565            let mut pipeline = self.ingest_pipeline();
14566            pipeline.ingest_formula(FormulaAstInput::Tree(ast), placement, None)?
14567        };
14568        self.graph.set_cell_formula_with_plan(
14569            sheet,
14570            row,
14571            col,
14572            ingested.ast_id,
14573            &ingested.dep_plan,
14574            ingested.dep_plan.volatile,
14575            ingested.dep_plan.dynamic,
14576        )?;
14577        for table in self.graph.empty_tables_at(placement) {
14578            self.graph.resize_table(&table.name, table.range)?;
14579        }
14580        self.clear_cell_format_state(sheet, placement);
14581        self.record_changed_cell(sheet, row, col);
14582
14583        // If the cell previously held a user value in the delta overlay, it must not continue
14584        // to mask the formula result under Arrow-canonical reads (overlay precedence is
14585        // delta -> computed -> base). Remove the overlay entry instead of writing `Empty`,
14586        // because an explicit `Empty` overlay would still take precedence over computed values.
14587        self.clear_delta_overlay_cell(sheet, row, col);
14588
14589        // Advance snapshot to reflect external mutation
14590        self.mark_topology_edited();
14591        Ok(())
14592    }
14593
14594    /// Bulk set many formulas on a sheet. Skips per-cell snapshot bumping and minimizes edge rebuilds.
14595    pub fn bulk_set_formulas<I>(&mut self, sheet: &str, items: I) -> Result<usize, ExcelError>
14596    where
14597        I: IntoIterator<Item = (u32, u32, ASTNode)>,
14598    {
14599        self.ensure_not_poisoned()?;
14600        let collected: Vec<(u32, u32, ASTNode)> = items.into_iter().collect();
14601        let edited_cells: Vec<(u32, u32)> = collected.iter().map(|(r, c, _)| (*r, *c)).collect();
14602        let sheet_id = self.graph.sheet_id_mut(sheet);
14603        let ingested = {
14604            let mut pipeline = self.ingest_pipeline();
14605            let inputs = collected.into_iter().map(|(row, col, ast)| {
14606                let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14607                (FormulaAstInput::Tree(ast), placement, None)
14608            });
14609            pipeline.ingest_batch(inputs)?
14610        };
14611        let planned: Vec<(u32, u32, AstNodeId, DependencyPlanRow)> = ingested
14612            .into_iter()
14613            .map(|formula| {
14614                (
14615                    formula.placement.coord.row() + 1,
14616                    formula.placement.coord.col() + 1,
14617                    formula.ast_id,
14618                    formula.dep_plan,
14619                )
14620            })
14621            .collect();
14622        let n = self.graph.bulk_set_formulas_with_plans(sheet, planned)?;
14623        for (row, col) in edited_cells {
14624            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14625            for table in self.graph.empty_tables_at(cell) {
14626                self.graph.resize_table(&table.name, table.range)?;
14627            }
14628            self.clear_cell_format_state(sheet, cell);
14629            self.record_changed_cell(sheet, row, col);
14630        }
14631        // Single topology bump after batch
14632        if n > 0 {
14633            self.mark_topology_edited();
14634        }
14635        Ok(n)
14636    }
14637
14638    /// Declare the formula cell at `sheet!(row, col)` (1-based) a dynamic
14639    /// array anchor whose spill identity comes from the source document, for
14640    /// example an XLSX `t="array"` formula bound to XLDAPR cell metadata.
14641    ///
14642    /// This changes only spill references to the anchor (`A1#`,
14643    /// `_xlfn.ANCHORARRAY(A1)`). A declared anchor with a committed
14644    /// multi-cell spill resolves to that spill, exactly as an undeclared
14645    /// one. Without a committed spill, a declared anchor whose current
14646    /// result is a scalar (not an error, so not a blocked `#SPILL!`
14647    /// anchor, and not empty) resolves to its own cell as a 1x1 reference,
14648    /// matching a spilled 1x1 dynamic array. Every other case keeps the
14649    /// `#REF!` of an undeclared anchor. Undeclared cells are unaffected, and
14650    /// fresh 1x1 results are not given spill identity by function name.
14651    ///
14652    /// The declaration belongs to the formula vertex. It is dropped when the
14653    /// cell's formula is replaced, the cell is cleared or overwritten with a
14654    /// value, the vertex is removed (including with its sheet), or a
14655    /// structural row/column edit moves the vertex. A structural edit that
14656    /// only adjusts the formula's references, without moving the anchor,
14657    /// keeps it. Declaring again re-establishes it.
14658    ///
14659    /// The anchor and its dependents are marked dirty, so the next
14660    /// evaluation observes the declaration.
14661    ///
14662    /// Returns `#REF!` if the sheet does not exist or the cell holds no
14663    /// formula vertex. A formula still staged by deferred graph building has
14664    /// no vertex: build the graph (for example [`Self::build_graph_all`])
14665    /// before declaring.
14666    pub fn declare_dynamic_array_anchor(
14667        &mut self,
14668        sheet: &str,
14669        row: u32,
14670        col: u32,
14671    ) -> Result<(), ExcelError> {
14672        self.ensure_not_poisoned()?;
14673        let not_a_formula = || {
14674            ExcelError::new(ExcelErrorKind::Ref)
14675                .with_message("Declared dynamic array anchor must be a formula cell")
14676        };
14677        if row == 0 || col == 0 {
14678            return Err(not_a_formula());
14679        }
14680        if self.get_staged_formula_text(sheet, row, col).is_some() {
14681            return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
14682                "Declared dynamic array anchor is still staged; build the graph first",
14683            ));
14684        }
14685        let sheet_id = self.graph.sheet_id(sheet).ok_or_else(not_a_formula)?;
14686        let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14687        let vertex = self
14688            .graph
14689            .cell_vertex_mut(&cell)
14690            .filter(|v| self.graph.vertex_has_formula(*v))
14691            .ok_or_else(not_a_formula)?;
14692        if self.graph.declare_dynamic_anchor(vertex, cell) {
14693            self.graph.mark_dirty_many(&[vertex]);
14694            self.mark_topology_edited();
14695        }
14696        Ok(())
14697    }
14698
14699    /// Declare a legacy array formula with a fixed output rectangle after
14700    /// ingestion and before evaluation. Coordinates are one-based. Replacement,
14701    /// removal or movement of the formula clears the declaration.
14702    /// Single-cell declarations do not register a spill anchor. Requires
14703    /// `EvalConfig::family_execution = false` to bypass family memoization.
14704    pub fn declare_fixed_array_formula(
14705        &mut self,
14706        sheet: &str,
14707        row: u32,
14708        col: u32,
14709        rows: u32,
14710        cols: u32,
14711    ) -> Result<(), ExcelError> {
14712        self.ensure_not_poisoned()?;
14713        if self.config.family_execution {
14714            return Err(ExcelError::new(ExcelErrorKind::Ref)
14715                .with_message("Fixed arrays require family_execution = false"));
14716        }
14717        if row == 0
14718            || col == 0
14719            || rows == 0
14720            || cols == 0
14721            || row.checked_add(rows - 1).is_none_or(|r| r > 1_048_576)
14722            || col.checked_add(cols - 1).is_none_or(|c| c > 16_384)
14723            || u64::from(rows) * u64::from(cols) > u64::from(self.config.spill.max_spill_cells)
14724        {
14725            return Err(ExcelError::new(ExcelErrorKind::Ref)
14726                .with_message("Invalid or over-cap fixed array extent"));
14727        }
14728        if self.get_staged_formula_text(sheet, row, col).is_some() {
14729            return Err(ExcelError::new(ExcelErrorKind::Ref)
14730                .with_message("Fixed array formula is still staged; build the graph first"));
14731        }
14732        let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
14733            ExcelError::new(ExcelErrorKind::Ref).with_message("Unknown fixed array sheet")
14734        })?;
14735        let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14736        let vertex = self
14737            .graph
14738            .cell_vertex_mut(&cell)
14739            .filter(|v| self.graph.vertex_has_formula(*v))
14740            .ok_or_else(|| {
14741                ExcelError::new(ExcelErrorKind::Ref)
14742                    .with_message("Fixed array anchor must be a formula cell")
14743            })?;
14744        if (rows == 1 && cols == 1 && self.graph.fixed_single_arrays.contains(&vertex))
14745            || self.graph.fixed_array_shapes.get(&vertex) == Some(&(rows, cols))
14746        {
14747            return Ok(());
14748        }
14749        if self.graph.fixed_single_arrays.contains(&vertex)
14750            || self.graph.spill_registry_has_anchor(vertex)
14751            || self.graph.is_current_declared_dynamic_anchor(vertex)
14752        {
14753            return Err(ExcelError::new(ExcelErrorKind::Ref)
14754                .with_message("Array declaration already exists; replace the formula first"));
14755        }
14756        if rows == 1 && cols == 1 {
14757            self.graph.fixed_single_arrays.insert(vertex);
14758        } else {
14759            let mut targets = Vec::with_capacity((rows * cols) as usize);
14760            for r in 0..rows {
14761                for c in 0..cols {
14762                    targets.push(self.graph.make_cell_ref(sheet, row + r, col + c));
14763                }
14764            }
14765            self.graph.plan_spill_region(vertex, &targets)?;
14766            // Reserve ownership before evaluation, even for Empty members.
14767            self.graph
14768                .commit_spill_region_atomic_with_fault(vertex, targets, vec![], None)?;
14769            self.graph.fixed_array_shapes.insert(vertex, (rows, cols));
14770        }
14771        self.graph.mark_dirty_many(&[vertex]);
14772        self.mark_topology_edited();
14773        Ok(())
14774    }
14775
14776    #[inline]
14777    fn normalize_public_cell_read(v: LiteralValue) -> Option<LiteralValue> {
14778        match v {
14779            LiteralValue::Empty => None,
14780            LiteralValue::Int(i) => Some(LiteralValue::Number(i as f64)),
14781            other => Some(other),
14782        }
14783    }
14784
14785    fn materialize_temporal_egress(
14786        value: LiteralValue,
14787        class: Option<&formualizer_common::numfmt::FormatClass>,
14788        policy: crate::engine::TemporalEgress,
14789        date_system: crate::engine::DateSystem,
14790    ) -> LiteralValue {
14791        use formualizer_common::numfmt::FormatClass;
14792        if policy == crate::engine::TemporalEgress::Serial {
14793            return value;
14794        }
14795        let LiteralValue::Number(serial) = value else {
14796            return value;
14797        };
14798        match class {
14799            Some(FormatClass::Date) if serial.fract() != 0.0 => {
14800                formualizer_common::try_serial_to_datetime_for(date_system, serial)
14801                    .map(LiteralValue::DateTime)
14802                    .unwrap_or(LiteralValue::Number(serial))
14803            }
14804            Some(FormatClass::Date) => {
14805                formualizer_common::try_serial_to_date_for(date_system, serial)
14806                    .map(LiteralValue::Date)
14807                    .unwrap_or(LiteralValue::Number(serial))
14808            }
14809            Some(FormatClass::DateTime) => {
14810                formualizer_common::try_serial_to_datetime_for(date_system, serial)
14811                    .map(LiteralValue::DateTime)
14812                    .unwrap_or(LiteralValue::Number(serial))
14813            }
14814            Some(FormatClass::Time) if (0.0..1.0).contains(&serial) => {
14815                let nanos = (serial * 86_400_000_000_000.0).round() as u64;
14816                let nanos = nanos.min(86_400_000_000_000 - 1);
14817                chrono::NaiveTime::from_num_seconds_from_midnight_opt(
14818                    (nanos / 1_000_000_000) as u32,
14819                    (nanos % 1_000_000_000) as u32,
14820                )
14821                .map(LiteralValue::Time)
14822                .unwrap_or(LiteralValue::Number(serial))
14823            }
14824            Some(FormatClass::Time | FormatClass::Duration) => {
14825                let nanos = (serial * 86_400.0 * 1_000_000_000.0).round();
14826                if nanos.is_finite() && nanos >= i64::MIN as f64 && nanos <= i64::MAX as f64 {
14827                    LiteralValue::Duration(chrono::Duration::nanoseconds(nanos as i64))
14828                } else {
14829                    LiteralValue::Number(serial)
14830                }
14831            }
14832            _ => LiteralValue::Number(serial),
14833        }
14834    }
14835
14836    pub(crate) fn effective_format_id(
14837        &self,
14838        sheet: &str,
14839        row: u32,
14840        col: u32,
14841    ) -> Option<crate::format::FormatId> {
14842        let arrow = self.arrow_sheets.sheet(sheet).and_then(|arrow| {
14843            arrow.format_id(
14844                row.saturating_sub(1) as usize,
14845                col.saturating_sub(1) as usize,
14846            )
14847        });
14848        arrow.or_else(|| {
14849            let sheet_id = self.graph.sheet_id(sheet)?;
14850            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
14851            self.derived_formats.get(&cell)
14852        })
14853    }
14854
14855    /// Get a cell value through the single temporal egress boundary.
14856    pub fn get_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
14857        let raw = self.read_cell_value(sheet, row, col)?;
14858        let format = self.effective_format_id(sheet, row, col);
14859        let class = format.and_then(|id| self.format_registry.class(id));
14860        Self::normalize_public_cell_read(Self::materialize_temporal_egress(
14861            raw,
14862            class,
14863            self.config.temporal_egress,
14864            self.config.date_system,
14865        ))
14866    }
14867
14868    /// Read a rectangular range through the temporal egress boundary.
14869    pub fn get_range_values(
14870        &self,
14871        sheet: &str,
14872        sr: u32,
14873        sc: u32,
14874        er: u32,
14875        ec: u32,
14876    ) -> Vec<Vec<LiteralValue>> {
14877        let height = er.saturating_sub(sr).saturating_add(1) as usize;
14878        let width = ec.saturating_sub(sc).saturating_add(1) as usize;
14879        let Some(asheet) = self.sheet_store().sheet(sheet) else {
14880            return vec![vec![LiteralValue::Empty; width]; height];
14881        };
14882        let view = asheet.range_view(
14883            sr.saturating_sub(1) as usize,
14884            sc.saturating_sub(1) as usize,
14885            er.saturating_sub(1) as usize,
14886            ec.saturating_sub(1) as usize,
14887        );
14888        let sheet_id = self.graph.sheet_id(sheet);
14889        let derived_formats = &self.derived_formats;
14890        let has_derived_formats =
14891            sheet_id.is_some_and(|sheet_id| derived_formats.any(|cell| cell.sheet_id == sheet_id));
14892        let mut out = Vec::with_capacity(height);
14893        if !asheet.has_formats() && !has_derived_formats {
14894            for rr in 0..height {
14895                let mut row = Vec::with_capacity(width);
14896                for cc in 0..width {
14897                    row.push(view.get_cell(rr, cc));
14898                }
14899                out.push(row);
14900            }
14901            return out;
14902        }
14903        let format_registry = &self.format_registry;
14904        for rr in 0..height {
14905            let mut row = Vec::with_capacity(width);
14906            for cc in 0..width {
14907                let raw = view.get_cell(rr, cc);
14908                let row0 = sr.saturating_sub(1).saturating_add(rr as u32);
14909                let col0 = sc.saturating_sub(1).saturating_add(cc as u32);
14910                let format = asheet.format_id(row0 as usize, col0 as usize).or_else(|| {
14911                    let cell = CellRef::new(sheet_id?, Coord::new(row0, col0, true, true));
14912                    derived_formats.get(&cell)
14913                });
14914                let class = format.and_then(|id| format_registry.class(id));
14915                row.push(Self::materialize_temporal_egress(
14916                    raw,
14917                    class,
14918                    self.config.temporal_egress,
14919                    self.config.date_system,
14920                ));
14921            }
14922            out.push(row);
14923        }
14924        out
14925    }
14926
14927    /// Unified internal read API for a single cell value (Arrow-truth).
14928    /// The 1x1 spill fallback of a declared dynamic-array anchor: its
14929    /// committed result is a non-error, non-empty scalar. Scheduling orders
14930    /// the anchor before its spill readers (the reference keeps a static
14931    /// edge to it), exactly as for committed multi-cell spills.
14932    fn declared_anchor_holds_scalar(&self, anchor: CellRef) -> bool {
14933        let sheet = self.graph.sheet_name(anchor.sheet_id);
14934        if sheet.is_empty() {
14935            return false;
14936        }
14937        matches!(
14938            self.read_cell_value(sheet, anchor.coord.row() + 1, anchor.coord.col() + 1),
14939            Some(value) if !matches!(
14940                value,
14941                LiteralValue::Error(_)
14942                    | LiteralValue::Array(_)
14943                    | LiteralValue::Pending
14944                    | LiteralValue::Empty
14945            )
14946        )
14947    }
14948
14949    pub(crate) fn read_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
14950        let asheet = self.sheet_store().sheet(sheet)?;
14951        let r0 = row.saturating_sub(1) as usize;
14952        let c0 = col.saturating_sub(1) as usize;
14953        let v = asheet.get_cell_value(r0, c0);
14954        if matches!(v, LiteralValue::Empty) {
14955            None
14956        } else {
14957            Some(v)
14958        }
14959    }
14960
14961    /// Unified internal read API for a range of cell values (Arrow-truth).
14962    pub(crate) fn read_range_values(
14963        &self,
14964        sheet: &str,
14965        sr: u32,
14966        sc: u32,
14967        er: u32,
14968        ec: u32,
14969    ) -> RangeView<'_> {
14970        let Some(asheet) = self.sheet_store().sheet(sheet) else {
14971            return RangeView::from_owned_rows(Vec::new(), self.config.date_system);
14972        };
14973        if er < sr || ec < sc {
14974            return asheet.range_view(1, 1, 0, 0);
14975        }
14976        let sr0 = sr.saturating_sub(1) as usize;
14977        let sc0 = sc.saturating_sub(1) as usize;
14978        let er0 = er.saturating_sub(1) as usize;
14979        let ec0 = ec.saturating_sub(1) as usize;
14980        asheet.range_view(sr0, sc0, er0, ec0)
14981    }
14982
14983    /// Get formula AST (if any) and current stored value for a cell
14984    pub fn get_cell(
14985        &self,
14986        sheet: &str,
14987        row: u32,
14988        col: u32,
14989    ) -> Option<(Option<formualizer_parse::ASTNode>, Option<LiteralValue>)> {
14990        let v = self.get_cell_value(sheet, row, col);
14991        let sheet_id = self.graph.sheet_id(sheet)?;
14992        let coord = Coord::from_excel(row, col, true, true);
14993        let cell = CellRef::new(sheet_id, coord);
14994        if let Some(vid) = self.graph.get_vertex_for_cell(&cell) {
14995            let ast = self.graph.get_formula(vid);
14996            Some((ast, v))
14997        } else if v.is_some() || self.graph.had_legacy_cell_vertex(&cell) {
14998            // A referenced or emptied value cell has no vertex (decision
14999            // 27), but it is a cell the graph knows, as it was when it had
15000            // one (the interactive formula edit routes on this).
15001            Some((None, v))
15002        } else {
15003            None
15004        }
15005    }
15006
15007    /// Begin batch operations - defer CSR rebuilds for better performance
15008    ///
15009    /// # Panics
15010    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
15011    pub fn begin_batch(&mut self) {
15012        self.assert_not_poisoned();
15013        self.graph.begin_batch();
15014    }
15015
15016    /// End batch operations and trigger CSR rebuild
15017    ///
15018    /// # Panics
15019    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
15020    pub fn end_batch(&mut self) {
15021        self.assert_not_poisoned();
15022        self.graph.end_batch();
15023    }
15024
15025    /// Begin a deferred-dirty scope for a multi-edit batch: while active,
15026    /// every edit's dirty propagation queues its sources instead of running
15027    /// a full BFS per edit, and the outermost `end_deferred_dirty` flushes
15028    /// the union with ONE multi-source propagation (O(component) instead of
15029    /// O(edits × component)). See `DependencyGraph::begin_deferred_dirty`.
15030    ///
15031    /// Callers MUST run `end_deferred_dirty` on every exit path, including
15032    /// error returns; evaluation entry points `debug_assert` no scope leaked.
15033    ///
15034    /// # Panics
15035    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
15036    pub fn begin_deferred_dirty(&mut self) {
15037        self.assert_not_poisoned();
15038        self.graph.begin_deferred_dirty();
15039    }
15040
15041    /// End a deferred-dirty scope, flushing the queued propagation when the
15042    /// outermost scope closes. See `Engine::begin_deferred_dirty`.
15043    ///
15044    /// # Panics
15045    /// See [Engine::is_poisoned] for the permanent poison refusal contract.
15046    pub fn end_deferred_dirty(&mut self) {
15047        self.assert_not_poisoned();
15048        let _ = self.graph.end_deferred_dirty();
15049    }
15050
15051    /// Total vertices processed by dirty-propagation BFS loops since graph
15052    /// creation. Perf-shape observability only (cross-crate tests assert
15053    /// batched edits propagate O(component), not O(edits × component)).
15054    pub fn dirty_propagation_visits(&self) -> u64 {
15055        self.graph.dirty_propagation_visits()
15056    }
15057
15058    /// Evaluate a single vertex.
15059    /// This is the core of the sequential evaluation logic for Milestone 3.1.
15060    #[inline]
15061    fn record_cell_if_changed(
15062        delta: &mut DeltaCollector,
15063        cell: &CellRef,
15064        old: &LiteralValue,
15065        new: &LiteralValue,
15066    ) {
15067        if old != new {
15068            delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
15069        }
15070    }
15071
15072    pub fn evaluate_vertex(&mut self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
15073        self.ensure_not_poisoned()?;
15074        self.observe_evaluation_resource_request(EvaluationRequestKind::Vertex, |engine| {
15075            engine.observe_function_semantic_epoch()?;
15076            // A direct request selects exactly one vertex, regardless of its formula kind.
15077            engine.resource_checkpoint(1)?;
15078            if !engine.graph.vertex_exists(vertex_id) {
15079                return engine.evaluate_vertex_impl(vertex_id, None);
15080            }
15081            let is_formula = matches!(
15082                engine.graph.get_vertex_kind(vertex_id),
15083                VertexKind::FormulaScalar | VertexKind::FormulaArray
15084            );
15085            if is_formula {
15086                engine.begin_evaluation_request();
15087                #[cfg(any(test, feature = "legacy_oracle"))]
15088                engine.graph.flush_pending_edge_deltas();
15089                let roots = [crate::engine::target_preparation::TargetProducer::Legacy(
15090                    vertex_id,
15091                )];
15092                engine.evaluate_legacy_target_roots(&roots, None)?;
15093            }
15094            if !is_formula {
15095                engine.refresh_resolution_snapshots();
15096            }
15097            engine.evaluate_vertex_impl(vertex_id, None)
15098        })
15099    }
15100
15101    /// Same rejection publication for owned arrays and pre-admitted range views.
15102    fn publish_oversized_spill(
15103        &mut self,
15104        vertex_id: VertexId,
15105        error: ExcelError,
15106        mut delta: Option<&mut DeltaCollector>,
15107    ) -> LiteralValue {
15108        self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
15109        let anchor = self
15110            .graph
15111            .get_cell_ref(vertex_id)
15112            .expect("cell ref for vertex");
15113        let spill_val = LiteralValue::Error(error);
15114        if let Some(d) = delta {
15115            let old = self
15116                .read_cell_value(
15117                    self.graph.sheet_name(anchor.sheet_id),
15118                    anchor.coord.row() + 1,
15119                    anchor.coord.col() + 1,
15120                )
15121                .unwrap_or(LiteralValue::Empty);
15122            if old != spill_val {
15123                d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
15124            }
15125        }
15126        self.graph.update_vertex_value_ref(vertex_id, &spill_val);
15127        if self.config.arrow_storage_enabled
15128            && self.config.delta_overlay_enabled
15129            && self.config.write_formula_overlay_enabled
15130        {
15131            let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
15132            self.mirror_value_to_computed_overlay(
15133                &sheet_name,
15134                anchor.coord.row() + 1,
15135                anchor.coord.col() + 1,
15136                &spill_val,
15137            );
15138        }
15139        spill_val
15140    }
15141
15142    fn evaluate_vertex_impl(
15143        &mut self,
15144        vertex_id: VertexId,
15145        delta: Option<&mut DeltaCollector>,
15146    ) -> Result<LiteralValue, ExcelError> {
15147        // Preserve the direct evaluator's compatibility behavior for invalid IDs, literal cells,
15148        // names, and other non-formula vertices. Only formula publication needs the C1a final
15149        // deadline checkpoint and effects pipeline.
15150        if !self.graph.vertex_exists(vertex_id) {
15151            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
15152                .with_message(format!("Vertex not found: {vertex_id:?}")));
15153        }
15154        if (self.active_resource_ledger.is_some()
15155            || self.graph.fixed_array_shapes.contains_key(&vertex_id))
15156            && matches!(
15157                self.graph.get_vertex_kind(vertex_id),
15158                VertexKind::FormulaScalar | VertexKind::FormulaArray
15159            )
15160        {
15161            let value = self
15162                .evaluate_vertex_immutable(vertex_id)
15163                .unwrap_or_else(LiteralValue::Error);
15164            let effects = self.plan_vertex_effects(vertex_id, value.clone())?;
15165            // Do not publish the selected result until the outer request's deadline succeeds.
15166            self.resource_checkpoint(0)?;
15167            let mut delta = delta;
15168            for effect in &effects {
15169                self.apply_effect_with_computed_writes(effect, delta.as_deref_mut(), None, None)?;
15170            }
15171            return Ok(value);
15172        }
15173
15174        let mut delta = delta;
15175
15176        // Get vertex kind and check if it needs evaluation
15177        let kind = self.graph.get_vertex_kind(vertex_id);
15178        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
15179
15180        let view = match kind {
15181            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
15182                if let Some(view) = self.graph.formula_view(vertex_id) {
15183                    view
15184                } else {
15185                    return Ok(LiteralValue::Number(0.0));
15186                }
15187            }
15188            VertexKind::Empty | VertexKind::Cell => {
15189                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
15190                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
15191                    let row = cell_ref.coord.row() + 1;
15192                    let col = cell_ref.coord.col() + 1;
15193                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
15194                        return Ok(v);
15195                    }
15196                }
15197                return Ok(LiteralValue::Number(0.0));
15198            }
15199            VertexKind::NamedScalar => {
15200                let value = self.evaluate_named_scalar(vertex_id, sheet_id)?;
15201                return Ok(value);
15202            }
15203            VertexKind::NamedArray => {
15204                let value = self.evaluate_named_array(vertex_id, sheet_id)?;
15205                return Ok(value);
15206            }
15207            VertexKind::InfiniteRange
15208            | VertexKind::Range
15209            | VertexKind::External
15210            | VertexKind::Table => {
15211                // Not directly evaluatable here.
15212                return Ok(LiteralValue::Number(0.0));
15213            }
15214        };
15215
15216        // The interpreter uses a reference to the engine as the context.
15217        let sheet_name = self.graph.sheet_name(sheet_id);
15218        let cell_ref = self
15219            .graph
15220            .get_cell_ref(vertex_id)
15221            .expect("cell ref for vertex");
15222        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
15223            .with_resolved_functions(&self.resolved_functions)
15224            .with_arena_cells(self);
15225
15226        let result = interpreter.evaluate_formula_view(
15227            view,
15228            self.graph.data_store(),
15229            self.graph.sheet_reg(),
15230        );
15231
15232        // If array result, perform spill from the anchor cell
15233        match result {
15234            Ok(cv) => {
15235                let derived_format = cv.format_id();
15236                self.record_derived_format(vertex_id, derived_format);
15237                let oversized_range = if matches!(cv, crate::traits::CalcValue::Range(_))
15238                    && self.graph.fixed_single_arrays.contains(&vertex_id)
15239                {
15240                    None
15241                } else {
15242                    crate::engine::result_finalization::range_spill_error(
15243                        &cv,
15244                        self.config.spill.max_spill_cells,
15245                    )
15246                };
15247                let is_oversized_range = oversized_range.is_some();
15248                let result_literal = if let Some(error) = oversized_range {
15249                    drop(cv);
15250                    LiteralValue::Error(error)
15251                } else {
15252                    self.materialize_formula_result(vertex_id, cv)
15253                };
15254                let output_sheet_name = sheet_name.to_string();
15255                self.write_computed_overlay_format_0based(
15256                    &output_sheet_name,
15257                    cell_ref.coord.row(),
15258                    cell_ref.coord.col(),
15259                    derived_format,
15260                );
15261                if is_oversized_range {
15262                    let LiteralValue::Error(error) = result_literal else {
15263                        unreachable!()
15264                    };
15265                    self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
15266                    return Ok(self.publish_oversized_spill(
15267                        vertex_id,
15268                        error,
15269                        delta.as_deref_mut(),
15270                    ));
15271                }
15272                match result_literal {
15273                    LiteralValue::Array(rows) => {
15274                        // Update kind to FormulaArray for tracking
15275                        self.graph
15276                            .set_kind(vertex_id, crate::engine::vertex::VertexKind::FormulaArray);
15277                        // Build target cells rectangle starting from anchor
15278                        let anchor = self
15279                            .graph
15280                            .get_cell_ref(vertex_id)
15281                            .expect("cell ref for vertex");
15282                        let sheet_id = anchor.sheet_id;
15283                        let h = rows.len() as u32;
15284                        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
15285
15286                        // Hard cap to avoid vertex explosion from huge dynamic arrays.
15287                        let spill_cells = (h as u64).saturating_mul(w as u64);
15288                        if spill_cells > self.config.spill.max_spill_cells as u64 {
15289                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
15290                                .with_message("SpillTooLarge")
15291                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
15292                                    expected_rows: h,
15293                                    expected_cols: w,
15294                                });
15295                            return Ok(self.publish_oversized_spill(
15296                                vertex_id,
15297                                spill_err,
15298                                delta.as_deref_mut(),
15299                            ));
15300                        }
15301                        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity)
15302                        const PACKED_MAX_ROW: u32 = 1_048_575; // 20-bit max
15303                        const PACKED_MAX_COL: u32 = 16_383; // 14-bit max
15304                        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
15305                        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
15306                        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
15307                            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
15308                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
15309                                .with_message("Spill exceeds sheet bounds")
15310                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
15311                                    expected_rows: h,
15312                                    expected_cols: w,
15313                                });
15314                            let spill_val = LiteralValue::Error(spill_err.clone());
15315                            if let Some(d) = delta.as_deref_mut() {
15316                                let old = self
15317                                    .read_cell_value(
15318                                        self.graph.sheet_name(anchor.sheet_id),
15319                                        anchor.coord.row() + 1,
15320                                        anchor.coord.col() + 1,
15321                                    )
15322                                    .unwrap_or(LiteralValue::Empty);
15323                                if old != spill_val {
15324                                    d.record_cell(
15325                                        anchor.sheet_id,
15326                                        anchor.coord.row(),
15327                                        anchor.coord.col(),
15328                                    );
15329                                }
15330                            }
15331                            self.graph.update_vertex_value_ref(vertex_id, &spill_val);
15332                            if self.config.arrow_storage_enabled
15333                                && self.config.delta_overlay_enabled
15334                                && self.config.write_formula_overlay_enabled
15335                            {
15336                                let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
15337                                self.mirror_value_to_computed_overlay(
15338                                    &sheet_name,
15339                                    anchor.coord.row() + 1,
15340                                    anchor.coord.col() + 1,
15341                                    &spill_val,
15342                                );
15343                            }
15344                            return Ok(spill_val);
15345                        }
15346                        let mut targets = Vec::new();
15347                        for r in 0..h {
15348                            for c in 0..w {
15349                                targets.push(self.graph.make_cell_ref_internal(
15350                                    sheet_id,
15351                                    anchor.coord.row() + r,
15352                                    anchor.coord.col() + c,
15353                                ));
15354                            }
15355                        }
15356
15357                        // Plan spill via spill manager shim
15358                        match self.spill_mgr.reserve(
15359                            vertex_id,
15360                            anchor,
15361                            SpillShape { rows: h, cols: w },
15362                            SpillMeta {
15363                                epoch: self.recalc_epoch,
15364                                config: self.config.spill,
15365                            },
15366                        ) {
15367                            Ok(()) => {
15368                                // Commit: write values to grid
15369                                // Default conflict policy is Error + FirstWins; reserve() enforces in-flight locks
15370                                // and plan_spill_region enforces overlap with committed formulas/spills/values.
15371                                if let Err(e) = self.commit_spill_and_mirror(
15372                                    vertex_id,
15373                                    &targets,
15374                                    rows.clone(),
15375                                    delta.as_deref_mut(),
15376                                ) {
15377                                    if e.kind != ExcelErrorKind::Spill {
15378                                        return Err(e);
15379                                    }
15380                                    // If commit fails, mark as error
15381                                    self.clear_spill_projection_and_mirror(
15382                                        vertex_id,
15383                                        delta.as_deref_mut(),
15384                                    );
15385                                    if let Some(d) = delta.as_deref_mut() {
15386                                        let old = self
15387                                            .read_cell_value(
15388                                                self.graph.sheet_name(anchor.sheet_id),
15389                                                anchor.coord.row() + 1,
15390                                                anchor.coord.col() + 1,
15391                                            )
15392                                            .unwrap_or(LiteralValue::Empty);
15393                                        let new = LiteralValue::Error(e.clone());
15394                                        if old != new {
15395                                            d.record_cell(
15396                                                anchor.sheet_id,
15397                                                anchor.coord.row(),
15398                                                anchor.coord.col(),
15399                                            );
15400                                        }
15401                                    }
15402                                    let err_val = LiteralValue::Error(e.clone());
15403                                    self.graph.update_vertex_value_ref(vertex_id, &err_val);
15404                                    if self.config.arrow_storage_enabled
15405                                        && self.config.delta_overlay_enabled
15406                                        && self.config.write_formula_overlay_enabled
15407                                    {
15408                                        let sheet_name =
15409                                            self.graph.sheet_name(anchor.sheet_id).to_string();
15410                                        self.mirror_value_to_computed_overlay(
15411                                            &sheet_name,
15412                                            anchor.coord.row() + 1,
15413                                            anchor.coord.col() + 1,
15414                                            &err_val,
15415                                        );
15416                                    }
15417                                    return Ok(err_val);
15418                                }
15419                                // Anchor shows the top-left value, like Excel
15420                                let top_left = rows
15421                                    .first()
15422                                    .and_then(|r| r.first())
15423                                    .cloned()
15424                                    .unwrap_or(LiteralValue::Empty);
15425                                self.graph.update_vertex_value_ref(vertex_id, &top_left);
15426                                Ok(top_left)
15427                            }
15428                            Err(e) => {
15429                                self.clear_spill_projection_and_mirror(
15430                                    vertex_id,
15431                                    delta.as_deref_mut(),
15432                                );
15433                                let spill_err = ExcelError::new(ExcelErrorKind::Spill)
15434                                    .with_message(e.message().unwrap_or("Spill blocked"))
15435                                    .with_extra(formualizer_common::ExcelErrorExtra::Spill {
15436                                        expected_rows: h,
15437                                        expected_cols: w,
15438                                    });
15439                                let spill_val = LiteralValue::Error(spill_err.clone());
15440                                if let Some(d) = delta.as_deref_mut() {
15441                                    let old = self
15442                                        .read_cell_value(
15443                                            self.graph.sheet_name(anchor.sheet_id),
15444                                            anchor.coord.row() + 1,
15445                                            anchor.coord.col() + 1,
15446                                        )
15447                                        .unwrap_or(LiteralValue::Empty);
15448                                    if old != spill_val {
15449                                        d.record_cell(
15450                                            anchor.sheet_id,
15451                                            anchor.coord.row(),
15452                                            anchor.coord.col(),
15453                                        );
15454                                    }
15455                                }
15456                                self.graph.update_vertex_value_ref(vertex_id, &spill_val);
15457                                if self.config.arrow_storage_enabled
15458                                    && self.config.delta_overlay_enabled
15459                                    && self.config.write_formula_overlay_enabled
15460                                {
15461                                    let sheet_name =
15462                                        self.graph.sheet_name(anchor.sheet_id).to_string();
15463                                    self.mirror_value_to_computed_overlay(
15464                                        &sheet_name,
15465                                        anchor.coord.row() + 1,
15466                                        anchor.coord.col() + 1,
15467                                        &spill_val,
15468                                    );
15469                                }
15470                                Ok(spill_val)
15471                            }
15472                        }
15473                    }
15474                    other => {
15475                        // Scalar result: store value and ensure any previous spill is cleared
15476                        let spill_cells = self.graph.spill_cells_to_clear(vertex_id);
15477                        if let Some(d) = delta.as_deref_mut()
15478                            && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
15479                        {
15480                            if spill_cells.is_empty() {
15481                                let old = self
15482                                    .read_cell_value(
15483                                        self.graph.sheet_name(anchor.sheet_id),
15484                                        anchor.coord.row() + 1,
15485                                        anchor.coord.col() + 1,
15486                                    )
15487                                    .unwrap_or(LiteralValue::Empty);
15488                                if old != other {
15489                                    d.record_cell(
15490                                        anchor.sheet_id,
15491                                        anchor.coord.row(),
15492                                        anchor.coord.col(),
15493                                    );
15494                                }
15495                            } else {
15496                                for cell in spill_cells.iter() {
15497                                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
15498                                    let old = self
15499                                        .read_cell_value(
15500                                            sheet_name,
15501                                            cell.coord.row() + 1,
15502                                            cell.coord.col() + 1,
15503                                        )
15504                                        .unwrap_or(LiteralValue::Empty);
15505                                    let new = if cell.sheet_id == anchor.sheet_id
15506                                        && cell.coord.row() == anchor.coord.row()
15507                                        && cell.coord.col() == anchor.coord.col()
15508                                    {
15509                                        other.clone()
15510                                    } else {
15511                                        LiteralValue::Empty
15512                                    };
15513                                    Self::record_cell_if_changed(d, cell, &old, &new);
15514                                }
15515                            }
15516                        }
15517                        self.graph.clear_spill_region(vertex_id);
15518                        self.freshness_note_spill_change(&spill_cells, &[]);
15519                        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
15520                            self.record_structural_change(scope);
15521                        }
15522                        if self.config.arrow_storage_enabled
15523                            && self.config.delta_overlay_enabled
15524                            && self.config.write_formula_overlay_enabled
15525                        {
15526                            let empty = LiteralValue::Empty;
15527                            for cell in spill_cells.iter() {
15528                                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
15529                                self.mirror_value_to_computed_overlay(
15530                                    &sheet_name,
15531                                    cell.coord.row() + 1,
15532                                    cell.coord.col() + 1,
15533                                    &empty,
15534                                );
15535                            }
15536                        }
15537                        self.graph.update_vertex_value_ref(vertex_id, &other);
15538                        // Optionally mirror into Arrow overlay for Arrow-backed reads
15539                        if self.config.arrow_storage_enabled
15540                            && self.config.delta_overlay_enabled
15541                            && self.config.write_formula_overlay_enabled
15542                        {
15543                            let anchor = self
15544                                .graph
15545                                .get_cell_ref(vertex_id)
15546                                .expect("cell ref for vertex");
15547                            let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
15548                            self.mirror_value_to_computed_overlay(
15549                                &sheet_name,
15550                                anchor.coord.row() + 1,
15551                                anchor.coord.col() + 1,
15552                                &other,
15553                            );
15554                        }
15555                        Ok(other)
15556                    }
15557                }
15558            }
15559            Err(e) => {
15560                // Runtime Excel error: store as a cell value instead of propagating
15561                // as an exception so bulk eval paths don't fail the whole pass.
15562                let spill_cells = self.graph.spill_cells_to_clear(vertex_id);
15563                let err_val = LiteralValue::Error(e.clone());
15564                if let Some(d) = delta
15565                    && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
15566                {
15567                    if spill_cells.is_empty() {
15568                        let old = self
15569                            .read_cell_value(
15570                                self.graph.sheet_name(anchor.sheet_id),
15571                                anchor.coord.row() + 1,
15572                                anchor.coord.col() + 1,
15573                            )
15574                            .unwrap_or(LiteralValue::Empty);
15575                        if old != err_val {
15576                            d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
15577                        }
15578                    } else {
15579                        for cell in spill_cells.iter() {
15580                            let sheet_name = self.graph.sheet_name(cell.sheet_id);
15581                            let old = self
15582                                .read_cell_value(
15583                                    sheet_name,
15584                                    cell.coord.row() + 1,
15585                                    cell.coord.col() + 1,
15586                                )
15587                                .unwrap_or(LiteralValue::Empty);
15588                            let new = if cell.sheet_id == anchor.sheet_id
15589                                && cell.coord.row() == anchor.coord.row()
15590                                && cell.coord.col() == anchor.coord.col()
15591                            {
15592                                err_val.clone()
15593                            } else {
15594                                LiteralValue::Empty
15595                            };
15596                            Self::record_cell_if_changed(d, cell, &old, &new);
15597                        }
15598                    }
15599                }
15600                self.graph.clear_spill_region(vertex_id);
15601                self.freshness_note_spill_change(&spill_cells, &[]);
15602                if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
15603                    self.record_structural_change(scope);
15604                }
15605                if self.config.arrow_storage_enabled
15606                    && self.config.delta_overlay_enabled
15607                    && self.config.write_formula_overlay_enabled
15608                {
15609                    let empty = LiteralValue::Empty;
15610                    for cell in spill_cells.iter() {
15611                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
15612                        self.mirror_value_to_computed_overlay(
15613                            &sheet_name,
15614                            cell.coord.row() + 1,
15615                            cell.coord.col() + 1,
15616                            &empty,
15617                        );
15618                    }
15619                }
15620                self.graph.update_vertex_value_ref(vertex_id, &err_val);
15621                if self.config.arrow_storage_enabled
15622                    && self.config.delta_overlay_enabled
15623                    && self.config.write_formula_overlay_enabled
15624                {
15625                    let anchor = self
15626                        .graph
15627                        .get_cell_ref(vertex_id)
15628                        .expect("cell ref for vertex");
15629                    let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
15630                    self.mirror_value_to_computed_overlay(
15631                        &sheet_name,
15632                        anchor.coord.row() + 1,
15633                        anchor.coord.col() + 1,
15634                        &err_val,
15635                    );
15636                }
15637                Ok(err_val)
15638            }
15639        }
15640    }
15641
15642    fn evaluate_named_scalar(
15643        &mut self,
15644        vertex_id: VertexId,
15645        sheet_id: SheetId,
15646    ) -> Result<LiteralValue, ExcelError> {
15647        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
15648            ExcelError::new(ExcelErrorKind::Name)
15649                .with_message("Named range metadata missing".to_string())
15650        })?;
15651
15652        match &named_range.definition {
15653            NamedDefinition::Cell(cell_ref) => {
15654                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
15655                let row = cell_ref.coord.row() + 1;
15656                let col = cell_ref.coord.col() + 1;
15657
15658                if let Some(dep_vertex) = self.graph.get_vertex_for_cell(cell_ref)
15659                    && matches!(
15660                        self.graph.get_vertex_kind(dep_vertex),
15661                        VertexKind::FormulaScalar | VertexKind::FormulaArray
15662                    )
15663                {
15664                    // Graph does not cache cell/formula values; ensure the precedent is evaluated.
15665                    let value = self.evaluate_vertex(dep_vertex)?;
15666                    self.graph.update_vertex_value_ref(vertex_id, &value);
15667                    Ok(value)
15668                } else {
15669                    let value = self
15670                        .read_cell_value(sheet_name, row, col)
15671                        .unwrap_or(LiteralValue::Empty);
15672                    self.graph.update_vertex_value_ref(vertex_id, &value);
15673                    Ok(value)
15674                }
15675            }
15676            NamedDefinition::Literal(v) => {
15677                let out = v.clone();
15678                self.graph.update_vertex_value_ref(vertex_id, &out);
15679                Ok(out)
15680            }
15681            NamedDefinition::Formula { ast, .. } => {
15682                let _origin = self
15683                    .operation_diagnostic_collector
15684                    .as_ref()
15685                    .map(|c| c.named_scope(vertex_id));
15686                let context_sheet = match named_range.scope {
15687                    NameScope::Sheet(id) => id,
15688                    NameScope::Workbook => sheet_id,
15689                };
15690                let sheet_name = self.graph.sheet_name(context_sheet);
15691                let cell_ref = self
15692                    .graph
15693                    .get_cell_ref(vertex_id)
15694                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
15695                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
15696                    .with_resolved_functions(&self.resolved_functions)
15697                    .with_arena_cells(self);
15698                match interpreter.evaluate_ast(ast) {
15699                    Ok(cv) => {
15700                        let value = cv.into_literal();
15701                        match value {
15702                            LiteralValue::Array(_) => {
15703                                let err = ExcelError::new(ExcelErrorKind::NImpl)
15704                                    .with_message("Array result in scalar named range".to_string());
15705                                let err_val = LiteralValue::Error(err.clone());
15706                                self.graph.update_vertex_value_ref(vertex_id, &err_val);
15707                                Ok(err_val)
15708                            }
15709                            other => {
15710                                self.graph.update_vertex_value_ref(vertex_id, &other);
15711                                Ok(other)
15712                            }
15713                        }
15714                    }
15715                    Err(err) => {
15716                        let err_val = LiteralValue::Error(err.clone());
15717                        self.graph.update_vertex_value_ref(vertex_id, &err_val);
15718                        Ok(err_val)
15719                    }
15720                }
15721            }
15722            NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
15723                .with_message("Range-valued name evaluated as scalar".to_string())),
15724        }
15725    }
15726
15727    fn evaluate_named_array(
15728        &mut self,
15729        vertex_id: VertexId,
15730        sheet_id: SheetId,
15731    ) -> Result<LiteralValue, ExcelError> {
15732        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
15733            ExcelError::new(ExcelErrorKind::Name)
15734                .with_message("Named range metadata missing".to_string())
15735        })?;
15736
15737        let out = match &named_range.definition {
15738            NamedDefinition::Range(range_ref) => {
15739                if range_ref.start.sheet_id != range_ref.end.sheet_id {
15740                    return Err(ExcelError::new(ExcelErrorKind::Ref)
15741                        .with_message("Named range cannot span sheets".to_string()));
15742                }
15743
15744                let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
15745                let sr0 = range_ref.start.coord.row();
15746                let sc0 = range_ref.start.coord.col();
15747                let er0 = range_ref.end.coord.row();
15748                let ec0 = range_ref.end.coord.col();
15749                if sr0 > er0 || sc0 > ec0 {
15750                    return Err(ExcelError::new(ExcelErrorKind::Ref)
15751                        .with_message("Invalid named range bounds".to_string()));
15752                }
15753
15754                let h = (er0 - sr0 + 1) as usize;
15755                let w = (ec0 - sc0 + 1) as usize;
15756                let cell_count = (h as u64).saturating_mul(w as u64);
15757                if cell_count > self.config.spill.max_spill_cells as u64 {
15758                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
15759                        "Named range too large to materialize as an array".to_string(),
15760                    ));
15761                }
15762
15763                let mut rows = Vec::with_capacity(h);
15764                for r0 in sr0..=er0 {
15765                    let mut row = Vec::with_capacity(w);
15766                    for c0 in sc0..=ec0 {
15767                        let v = self
15768                            .read_cell_value(sheet_name, r0 + 1, c0 + 1)
15769                            .unwrap_or(LiteralValue::Empty);
15770                        row.push(v);
15771                    }
15772                    rows.push(row);
15773                }
15774                LiteralValue::Array(rows)
15775            }
15776            NamedDefinition::Cell(cell_ref) => {
15777                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
15778                let row = cell_ref.coord.row() + 1;
15779                let col = cell_ref.coord.col() + 1;
15780                let v = self
15781                    .read_cell_value(sheet_name, row, col)
15782                    .unwrap_or(LiteralValue::Empty);
15783                LiteralValue::Array(vec![vec![v]])
15784            }
15785            NamedDefinition::Literal(v) => LiteralValue::Array(vec![vec![v.clone()]]),
15786            NamedDefinition::Formula { ast, .. } => {
15787                let _origin = self
15788                    .operation_diagnostic_collector
15789                    .as_ref()
15790                    .map(|c| c.named_scope(vertex_id));
15791                let context_sheet = match named_range.scope {
15792                    NameScope::Sheet(id) => id,
15793                    NameScope::Workbook => sheet_id,
15794                };
15795                let sheet_name = self.graph.sheet_name(context_sheet);
15796                let cell_ref = self
15797                    .graph
15798                    .get_cell_ref(vertex_id)
15799                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
15800                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
15801                    .with_resolved_functions(&self.resolved_functions)
15802                    .with_arena_cells(self);
15803                match interpreter.evaluate_ast(ast) {
15804                    Ok(cv) => {
15805                        let v = cv.into_literal();
15806                        match v {
15807                            LiteralValue::Array(_) => v,
15808                            other => LiteralValue::Array(vec![vec![other]]),
15809                        }
15810                    }
15811                    Err(err) => LiteralValue::Error(err),
15812                }
15813            }
15814        };
15815
15816        self.graph.update_vertex_value_ref(vertex_id, &out);
15817        Ok(out)
15818    }
15819
15820    fn replan_exhausted_error(&self, limit: usize, context: &str) -> ExcelError {
15821        crate::engine::ResourceLedgerError::Exhausted(
15822            formualizer_common::ResourceExhaustionDetail {
15823                reason: formualizer_common::ResourceExhaustionReason::WorkUnits,
15824                limit: limit as u64,
15825                observed: limit.saturating_add(1) as u64,
15826                request_id: self
15827                    .active_evaluation_resource_request
15828                    .as_ref()
15829                    .map(|stats| stats.request_id),
15830            },
15831        )
15832        .into_excel_error()
15833        .with_message(format!("{context} did not converge after {limit} replans"))
15834    }
15835
15836    fn transient_target_preparation_stale(error: &ExcelError) -> bool {
15837        matches!(
15838            error.extra(),
15839            formualizer_common::ExcelErrorExtra::PreparationStale {
15840                reason: formualizer_common::PreparationStaleReason::Semantic
15841                    | formualizer_common::PreparationStaleReason::Provider
15842            }
15843        )
15844    }
15845
15846    fn prepare_graph_for_routed_evaluation(
15847        &mut self,
15848        targets: &[crate::engine::EvaluationTarget],
15849        options: &crate::engine::TargetEvalOptions<'_>,
15850    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
15851        const MAX_TRANSIENT_PREPARATION_RETRIES: usize = 2;
15852        let mut retries = 0usize;
15853        loop {
15854            match self.prepare_graph_for_targets_unobserved(targets, options) {
15855                Err(error)
15856                    if Self::transient_target_preparation_stale(&error)
15857                        && retries < MAX_TRANSIENT_PREPARATION_RETRIES =>
15858                {
15859                    retries = retries.saturating_add(1);
15860                }
15861                result => return result,
15862            }
15863        }
15864    }
15865
15866    fn evaluate_mixed_targets(
15867        &mut self,
15868        targets: &[crate::engine::EvaluationTarget],
15869        delta: Option<&mut DeltaCollector>,
15870    ) -> Result<EvalResult, ExcelError> {
15871        let _source_cache = self.source_cache_session();
15872        let cancel = self.active_cancel_flag.clone();
15873        let options = crate::engine::TargetEvalOptions {
15874            request_id: self
15875                .active_evaluation_resource_request
15876                .as_ref()
15877                .map(|stats| stats.request_id),
15878            cancel,
15879            deadline: None,
15880            budgets: None,
15881            opaque_policy: crate::engine::OpaquePreparePolicy::Widen,
15882        };
15883        self.prepare_and_execute_target_recipe(targets, &options, delta)
15884    }
15885
15886    fn prepare_and_execute_target_recipe(
15887        &mut self,
15888        targets: &[crate::engine::EvaluationTarget],
15889        options: &crate::engine::TargetEvalOptions<'_>,
15890        delta: Option<&mut DeltaCollector>,
15891    ) -> Result<EvalResult, ExcelError> {
15892        let preparation = self.prepare_graph_for_routed_evaluation(targets, options)?;
15893        self.execute_prepared_target_recipe(targets, &preparation.widened_scope, delta)
15894    }
15895
15896    fn execute_prepared_target_recipe(
15897        &mut self,
15898        targets: &[crate::engine::EvaluationTarget],
15899        scope: &crate::engine::PrepareScope,
15900        delta: Option<&mut DeltaCollector>,
15901    ) -> Result<EvalResult, ExcelError> {
15902        self.require_unified_authority()?;
15903        if matches!(scope, crate::engine::PrepareScope::Workbook)
15904            && let Some(stats) = self.active_evaluation_resource_request.as_mut()
15905        {
15906            stats.workbook_exact_attempts = stats.workbook_exact_attempts.max(1);
15907        }
15908        let mut roots = self.resolve_target_producers(targets)?;
15909        if let crate::engine::PrepareScope::Sheets(sheets) = scope {
15910            let request_id = self
15911                .active_evaluation_resource_request
15912                .as_ref()
15913                .map(|request| request.request_id);
15914            let root_count = roots.len();
15915            let mut widened_roots =
15916                OrderedTargetProducers::from_ordered(std::mem::take(&mut roots))
15917                    .map_err(|_| target_root_allocation_error(root_count, request_id))?;
15918            let sheet_ids = sheets
15919                .iter()
15920                .filter_map(|sheet| self.graph.sheet_id(sheet))
15921                .collect::<FxHashSet<_>>();
15922            for vertex in self.graph.formula_vertices() {
15923                if sheet_ids.contains(&self.graph.get_vertex_sheet_id(vertex)) {
15924                    widened_roots
15925                        .push(crate::engine::target_preparation::TargetProducer::Legacy(
15926                            vertex,
15927                        ))
15928                        .map_err(|_| {
15929                            target_root_allocation_error(widened_roots.len() + 1, request_id)
15930                        })?;
15931                }
15932            }
15933            roots = widened_roots.into_vec();
15934        }
15935        self.begin_evaluation_request();
15936        #[cfg(any(test, feature = "legacy_oracle"))]
15937        self.graph.flush_pending_edge_deltas();
15938        let workbook_scope = matches!(scope, crate::engine::PrepareScope::Workbook);
15939        if workbook_scope {
15940            if let Some(delta) = delta {
15941                self.evaluate_all_with_delta_collector(delta)
15942            } else {
15943                self.evaluate_all_legacy_impl()
15944            }
15945        } else {
15946            self.evaluate_legacy_target_roots(&roots, delta)
15947        }
15948    }
15949
15950    fn legacy_coordinate_targets(
15951        &mut self,
15952        targets: &[(&str, u32, u32)],
15953    ) -> Vec<crate::engine::EvaluationTarget> {
15954        targets
15955            .iter()
15956            .map(|(sheet, row, col)| {
15957                // Compatibility APIs historically interned an unknown target sheet
15958                // and returned an empty value rather than rejecting the target.
15959                self.graph.sheet_id_mut(sheet);
15960                crate::engine::EvaluationTarget::Cell {
15961                    sheet: (*sheet).to_string(),
15962                    row: *row,
15963                    col: *col,
15964                }
15965            })
15966            .collect()
15967    }
15968
15969    /// Evaluate the necessary mixed producer closure for typed cell, range, name, and table targets.
15970    pub fn evaluate_targets(
15971        &mut self,
15972        targets: &[crate::engine::EvaluationTarget],
15973    ) -> Result<EvalResult, ExcelError> {
15974        self.ensure_not_poisoned()?;
15975        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
15976            engine.observe_function_semantic_epoch()?;
15977            engine.validate_deterministic_mode()?;
15978            engine.evaluate_mixed_targets(targets, None)
15979        })
15980    }
15981
15982    /// Evaluate typed targets with explicit preparation policy and request controls.
15983    pub fn evaluate_targets_with_options(
15984        &mut self,
15985        targets: &[crate::engine::EvaluationTarget],
15986        options: crate::engine::TargetEvalOptions<'_>,
15987    ) -> Result<EvalResult, ExcelError> {
15988        self.ensure_not_poisoned()?;
15989        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
15990            engine.active_cancel_flag = options.cancel.clone();
15991            engine.active_evaluation_deadline = options.deadline;
15992            let result = (|| {
15993                engine.cancellation_checkpoint("Evaluation cancelled before target preparation")?;
15994                engine.observe_function_semantic_epoch()?;
15995                engine.validate_deterministic_mode()?;
15996                let _source_cache = engine.source_cache_session();
15997                engine.prepare_and_execute_target_recipe(targets, &options, None)
15998            })();
15999            engine.active_cancel_flag = None;
16000            engine.active_evaluation_deadline = None;
16001            result
16002        })
16003    }
16004
16005    /// Evaluate typed targets and return the versioned run/region delta for the request.
16006    pub fn evaluate_targets_with_delta(
16007        &mut self,
16008        targets: &[crate::engine::EvaluationTarget],
16009    ) -> Result<(EvalResult, crate::engine::TargetEvalDelta), ExcelError> {
16010        self.ensure_not_poisoned()?;
16011        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
16012            engine.observe_function_semantic_epoch()?;
16013            engine.validate_deterministic_mode()?;
16014            let mut collector = DeltaCollector::new(DeltaMode::Cells);
16015            let result = engine.evaluate_mixed_targets(targets, Some(&mut collector))?;
16016            Ok((result, collector.finish_target()))
16017        })
16018    }
16019
16020    /// Evaluate only the necessary precedents for specific target cells (demand-driven)
16021    pub fn evaluate_until(
16022        &mut self,
16023        targets: &[(&str, u32, u32)],
16024    ) -> Result<EvalResult, ExcelError> {
16025        self.ensure_not_poisoned()?;
16026        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
16027            engine.evaluate_until_unobserved(targets)
16028        })
16029    }
16030
16031    fn evaluate_until_unobserved(
16032        &mut self,
16033        targets: &[(&str, u32, u32)],
16034    ) -> Result<EvalResult, ExcelError> {
16035        self.observe_function_semantic_epoch()?;
16036        let targets = self.legacy_coordinate_targets(targets);
16037        self.evaluate_mixed_targets(&targets, None)
16038    }
16039
16040    fn evaluate_until_with_delta_collector(
16041        &mut self,
16042        targets: &[(&str, u32, u32)],
16043        delta: &mut DeltaCollector,
16044    ) -> Result<EvalResult, ExcelError> {
16045        let targets = self.legacy_coordinate_targets(targets);
16046        self.evaluate_mixed_targets(&targets, Some(delta))
16047    }
16048
16049    fn evaluate_legacy_target_roots(
16050        &mut self,
16051        roots: &[crate::engine::target_preparation::TargetProducer],
16052        mut delta: Option<&mut DeltaCollector>,
16053    ) -> Result<EvalResult, ExcelError> {
16054        use crate::engine::target_preparation::TargetProducer;
16055        #[cfg(any(test, feature = "benchmark_internal"))]
16056        {
16057            self.recalc_reuse_probe
16058                .get_mut()
16059                .unwrap()
16060                .legacy_target_requests += 1;
16061        }
16062        let start = crate::instant::FzInstant::now();
16063        let root_vertices = roots
16064            .iter()
16065            .filter_map(|root| match root {
16066                TargetProducer::Legacy(vertex) | TargetProducer::Symbol(vertex) => Some(*vertex),
16067                TargetProducer::ValueOnly(_) => None,
16068            })
16069            .collect::<Vec<_>>();
16070        let mut computed_vertices = 0usize;
16071        let mut cycle_errors = 0usize;
16072        let mut replans = 0usize;
16073        const MAX_REPLAN: usize = 5;
16074        self.graph.authority_sync();
16075        loop {
16076            let (precedents_to_eval, old_vdeps) = self.demand_subgraph(&root_vertices)?;
16077            if precedents_to_eval.is_empty() {
16078                break;
16079            }
16080            #[cfg(any(test, feature = "benchmark_internal"))]
16081            {
16082                self.recalc_reuse_probe
16083                    .get_mut()
16084                    .unwrap()
16085                    .target_schedule_builds += 1;
16086            }
16087            let schedule = {
16088                self.graph.authority_sync();
16089                let mut ledger = self.active_resource_ledger.take();
16090                let result = self.create_authority_schedule(
16091                    &precedents_to_eval,
16092                    &old_vdeps,
16093                    ledger.as_mut(),
16094                );
16095                self.active_resource_ledger = ledger;
16096                result?
16097            };
16098            self.begin_pass(&schedule);
16099            for (unit_index, &unit) in schedule.units.iter().enumerate() {
16100                self.cancellation_checkpoint("Evaluation cancelled before target schedule unit")?;
16101                match unit {
16102                    ScheduleUnit::Cycle(index) => {
16103                        if self.handle_cycle_unit(
16104                            schedule.unit_cycle(index),
16105                            delta.as_deref_mut(),
16106                            None,
16107                            None,
16108                        )? > 0
16109                        {
16110                            cycle_errors = cycle_errors.saturating_add(1);
16111                        }
16112                    }
16113                    ScheduleUnit::Layer(index) => {
16114                        let layer = schedule.unit_layer(index);
16115                        let evaluated = if let Some(delta) = delta.as_deref_mut() {
16116                            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16117                                self.evaluate_layer_parallel_with_delta(layer, delta)?
16118                            } else {
16119                                self.evaluate_layer_sequential_with_delta(layer, delta)?
16120                            }
16121                        } else if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16122                            self.evaluate_layer_parallel(layer)?
16123                        } else {
16124                            self.evaluate_layer_sequential(layer)?
16125                        };
16126                        computed_vertices = computed_vertices.saturating_add(evaluated);
16127                    }
16128                }
16129                if self.stop_after_unit(&schedule, unit_index) {
16130                    break;
16131                }
16132            }
16133            let changed = self.changed_virtual_dep_vertices(&precedents_to_eval, &old_vdeps);
16134            self.resource_checkpoint(0)?;
16135            if !self.finish_target_pass_dirty(&precedents_to_eval, &changed) {
16136                break;
16137            }
16138            if replans >= MAX_REPLAN {
16139                return Err(self.replan_exhausted_error(
16140                    MAX_REPLAN,
16141                    "targeted legacy dynamic dependency evaluation",
16142                ));
16143            }
16144            replans = replans.saturating_add(1);
16145        }
16146        self.redirty_for_next_recalc();
16147        Ok(EvalResult {
16148            computed_vertices,
16149            cycle_errors,
16150            elapsed: start.elapsed(),
16151        })
16152    }
16153
16154    /// Build a revision-bound compatibility plan covering every prepared formula vertex.
16155    pub fn build_recalc_plan(&self) -> Result<RecalcPlan, ExcelError> {
16156        self.ensure_not_poisoned()?;
16157        if self.has_staged_formulas() || self.staged_formula_index.has_packages() {
16158            return Err(
16159                Self::plan_stale(formualizer_common::PlanStaleReason::Staged).with_message(
16160                    "compatibility recalculation plans require all staged formulas to be prepared",
16161                ),
16162            );
16163        }
16164        let key = self.recalc_plan_key();
16165        let mut vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
16166        vertices.sort_unstable();
16167        let has_dynamic_refs = vertices.iter().copied().any(|v| self.graph.is_dynamic(v));
16168        let schedule = if vertices.is_empty() {
16169            crate::engine::Schedule {
16170                units: Vec::new(),
16171                layers: Vec::new(),
16172                cycles: Vec::new(),
16173            }
16174        } else {
16175            self.create_evaluation_schedule_uncached(&vertices, None)?.0
16176        };
16177        self.validate_recalc_plan_key(&key)?;
16178        Ok(RecalcPlan {
16179            key,
16180            kind: RecalcPlanKind::CompatibilityFull {
16181                schedule,
16182                has_dynamic_refs,
16183            },
16184        })
16185    }
16186
16187    /// Prepare stable typed targets and retain a revision-bound run-local recipe.
16188    pub fn build_recalc_plan_for_targets(
16189        &mut self,
16190        targets: &[crate::engine::EvaluationTarget],
16191    ) -> Result<RecalcPlan, ExcelError> {
16192        self.ensure_not_poisoned()?;
16193        self.build_recalc_plan_for_targets_with_options(
16194            targets,
16195            crate::engine::TargetEvalOptions::default(),
16196        )
16197    }
16198
16199    pub fn build_recalc_plan_for_targets_with_options(
16200        &mut self,
16201        targets: &[crate::engine::EvaluationTarget],
16202        options: crate::engine::TargetEvalOptions<'_>,
16203    ) -> Result<RecalcPlan, ExcelError> {
16204        self.ensure_not_poisoned()?;
16205        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
16206            engine.observe_function_semantic_epoch()?;
16207            engine.validate_deterministic_mode()?;
16208            let _source_cache = engine.source_cache_session();
16209            let preparation = engine.prepare_graph_for_routed_evaluation(targets, &options)?;
16210            #[cfg(any(test, feature = "legacy_oracle"))]
16211            engine.graph.flush_pending_edge_deltas();
16212            let topology = if matches!(
16213                preparation.widened_scope,
16214                crate::engine::PrepareScope::Workbook
16215            ) {
16216                RecalcTopology::Workbook
16217            } else {
16218                RecalcTopology::RunLocalRecipe
16219            };
16220            Ok(RecalcPlan {
16221                key: engine.recalc_plan_key(),
16222                kind: RecalcPlanKind::Target {
16223                    targets: targets.to_vec(),
16224                    scope: preparation.widened_scope,
16225                    topology,
16226                    dynamic_policy: DynamicPlanPolicy::BoundedTargetReplan,
16227                },
16228            })
16229        })
16230    }
16231
16232    /// Evaluate using a previously constructed compatibility or target plan.
16233    pub fn evaluate_recalc_plan(&mut self, plan: &RecalcPlan) -> Result<EvalResult, ExcelError> {
16234        self.ensure_not_poisoned()?;
16235        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
16236            engine.evaluate_recalc_plan_unobserved(plan)
16237        })
16238    }
16239
16240    pub fn evaluate_recalc_plan_with_controls(
16241        &mut self,
16242        plan: &RecalcPlan,
16243        cancel: Option<crate::engine::CancelToken>,
16244        deadline: Option<Instant>,
16245    ) -> Result<EvalResult, ExcelError> {
16246        self.ensure_not_poisoned()?;
16247        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
16248            engine.active_cancel_flag = cancel.clone();
16249            engine.active_evaluation_deadline = deadline;
16250            let result = engine.evaluate_recalc_plan_unobserved(plan);
16251            engine.active_cancel_flag = None;
16252            engine.active_evaluation_deadline = None;
16253            result
16254        })
16255    }
16256
16257    fn evaluate_recalc_plan_unobserved(
16258        &mut self,
16259        plan: &RecalcPlan,
16260    ) -> Result<EvalResult, ExcelError> {
16261        #[cfg(any(test, feature = "legacy_oracle"))]
16262        self.graph.flush_pending_edge_deltas();
16263        self.validate_recalc_plan_key(&plan.key)?;
16264        self.cancellation_checkpoint("Evaluation cancelled before recalculation plan execution")?;
16265        self.validate_deterministic_mode()?;
16266
16267        match &plan.kind {
16268            RecalcPlanKind::Target {
16269                targets,
16270                scope,
16271                topology,
16272                dynamic_policy,
16273            } => {
16274                debug_assert_eq!(*dynamic_policy, DynamicPlanPolicy::BoundedTargetReplan);
16275                debug_assert_eq!(
16276                    matches!(topology, RecalcTopology::Workbook),
16277                    matches!(scope, crate::engine::PrepareScope::Workbook)
16278                );
16279                let _source_cache = self.source_cache_session();
16280                self.execute_prepared_target_recipe(targets, scope, None)
16281            }
16282            RecalcPlanKind::CompatibilityFull {
16283                schedule,
16284                has_dynamic_refs,
16285            } => {
16286                let _source_cache = self.source_cache_session();
16287                self.begin_evaluation_request();
16288                if *has_dynamic_refs {
16289                    self.virtual_dep_fallback_activations =
16290                        self.virtual_dep_fallback_activations.saturating_add(1);
16291                    return self.evaluate_all_coordinator();
16292                }
16293
16294                let start = crate::instant::FzInstant::now();
16295                let dirty_vertices = self.graph.get_evaluation_vertices();
16296                if dirty_vertices.is_empty() {
16297                    return Ok(EvalResult {
16298                        computed_vertices: 0,
16299                        cycle_errors: 0,
16300                        elapsed: start.elapsed(),
16301                    });
16302                }
16303
16304                let dirty_set: FxHashSet<VertexId> = dirty_vertices.iter().copied().collect();
16305                let mut computed_vertices = 0;
16306                let mut cycle_errors = 0;
16307                for &unit in &schedule.units {
16308                    self.cancellation_checkpoint(
16309                        "Evaluation cancelled before recalculation plan schedule unit",
16310                    )?;
16311                    match unit {
16312                        ScheduleUnit::Cycle(i) => {
16313                            let stamped = self.handle_cycle_unit(
16314                                schedule.unit_cycle(i),
16315                                None,
16316                                Some(&dirty_set),
16317                                None,
16318                            )?;
16319                            if stamped > 0 {
16320                                cycle_errors += 1;
16321                            }
16322                        }
16323                        ScheduleUnit::Layer(i) => {
16324                            let work: Vec<VertexId> = schedule
16325                                .unit_layer(i)
16326                                .vertices
16327                                .iter()
16328                                .copied()
16329                                .filter(|v| dirty_set.contains(v))
16330                                .collect();
16331                            if work.is_empty() {
16332                                continue;
16333                            }
16334                            let temp_layer = crate::engine::scheduler::Layer::new(work);
16335                            if self.thread_pool.is_some() && temp_layer.vertices.len() > 1 {
16336                                computed_vertices += self.evaluate_layer_parallel(&temp_layer)?;
16337                            } else {
16338                                computed_vertices += self.evaluate_layer_sequential(&temp_layer)?;
16339                            }
16340                        }
16341                    }
16342                }
16343
16344                self.resource_checkpoint(0)?;
16345                self.graph.clear_dirty_flags(&dirty_vertices);
16346                self.redirty_for_next_recalc();
16347                Ok(EvalResult {
16348                    computed_vertices,
16349                    cycle_errors,
16350                    elapsed: start.elapsed(),
16351                })
16352            }
16353        }
16354    }
16355}
16356
16357impl<R> Engine<R>
16358where
16359    R: EvaluationContext,
16360{
16361    /// Refuse out-of-scope authority states before evaluation can demote spans
16362    /// or execute a legacy schedule. The public error type is unchanged; NImpl
16363    /// carries the exact internal Unsupported operation for the deferred-scope
16364    /// gate. Admission and allocation failures are not scope exceptions.
16365    fn require_unified_authority(&mut self) -> Result<(), ExcelError> {
16366        self.graph
16367            .authority()
16368            .map(|_| ())
16369            .map_err(Self::authority_excel_error)
16370    }
16371
16372    fn authority_excel_error(error: crate::engine::authority::store::AuthorityError) -> ExcelError {
16373        use crate::engine::authority::store::AuthorityError;
16374        // Some unchanged behavioral tests assert only `error.kind`, hiding
16375        // the operation in their panic. The opt-in gate trace proves which
16376        // typed error was actually returned; it never changes that error.
16377        #[cfg(test)]
16378        if std::env::var_os("FZ_AUTHORITY_DEFERRED_TRACE").is_some() {
16379            eprintln!("M1B_AUTHORITY_ERROR {error:?}");
16380        }
16381        let kind = match error {
16382            AuthorityError::Unsupported { .. } => ExcelErrorKind::NImpl,
16383            _ => ExcelErrorKind::Error,
16384        };
16385        ExcelError::new(kind).with_message(format!("unified_authority: {error:?}"))
16386    }
16387
16388    /// Iterative members whose next-request invalidation was deferred by a
16389    /// completed snapshot. This does not establish source-cache seed provenance.
16390    pub fn pending_snapshot_iterative_members(&self) -> usize {
16391        self.snapshot_iterative_redirty_pending.len()
16392    }
16393
16394    /// Evaluate a current result snapshot, optionally with cancellation.
16395    ///
16396    /// This evaluates exactly like [`Self::evaluate_all`] (or its cancellable
16397    /// counterpart), but leaves volatile vertices and their dependents Current
16398    /// at return rather than marking them dirty for the next cycle. It does
16399    /// not suppress iterative-SCC redirty or any other freshness check.
16400    ///
16401    /// Callers retaining the engine must start another evaluation request
16402    /// (`evaluate_all`, `evaluate_until`, `evaluate_cell`, or equivalent) to
16403    /// refresh volatile values. That request restores the deferred volatile
16404    /// redirty before selecting its work. Iterative SCC invalidation is deferred
16405    /// on the same boundary, preserving completed snapshot values as Current.
16406    /// Errors and cancellation never leave the snapshot mode active.
16407    pub fn evaluate_all_for_snapshot(
16408        &mut self,
16409        cancel: Option<crate::engine::CancelToken>,
16410    ) -> Result<EvalResult, ExcelError> {
16411        self.ensure_not_poisoned()?;
16412        // Refusal must precede the scope and its failure cleanup: redirtying
16413        // volatiles here would invalidate values installed by the active job.
16414        if self.has_active_recalc_job() {
16415            return Err(Self::active_recalc_job_error());
16416        }
16417        /// Leaves snapshot mode even when evaluation unwinds. A panic
16418        /// keeps the deferred volatile redirty pending, so the next request
16419        /// restores volatile work without touching the graph mid-unwind.
16420        struct SnapshotScope<'e, R: EvaluationContext>(&'e mut Engine<R>);
16421        impl<R: EvaluationContext> Drop for SnapshotScope<'_, R> {
16422            fn drop(&mut self) {
16423                self.0.snapshot_evaluation_active = false;
16424                if std::thread::panicking() {
16425                    self.0.snapshot_volatile_redirty_pending = true;
16426                }
16427            }
16428        }
16429        self.snapshot_evaluation_active = true;
16430        let result = {
16431            let scope = SnapshotScope(self);
16432            match cancel {
16433                Some(cancel) => scope.0.evaluate_all_cancellable(cancel),
16434                None => scope.0.evaluate_all(),
16435            }
16436        };
16437        if result.is_err() {
16438            self.snapshot_volatile_redirty_pending = false;
16439            self.graph.redirty_volatiles();
16440        }
16441        result
16442    }
16443
16444    /// Evaluate until a soft boundary, preserving installed work for a later call.
16445    /// A family run or cycle may overrun the budget. Cancellation keeps its
16446    /// existing post-work boundary: the observing unit is not completed.
16447    ///
16448    /// Results are independent of call boundaries, except for iterative cycles
16449    /// created by edits during a pending recalculation. Such new cycles start
16450    /// from the cells' current values when they form, including values installed
16451    /// by earlier calls; they need not match a fresh, uninterrupted calculation.
16452    /// SCCs already present at the start retain their original recalc seeds.
16453    ///
16454    /// Function providers without a semantic revision (`planning_semantic_revision`
16455    /// returns `None`) are assumed stable for the duration of a logical
16456    /// recalculation. Hosts changing provider behaviour must bump its semantic
16457    /// revision or mark the affected cells dirty.
16458    ///
16459    /// Load-time mutation APIs, including `ArrowBulkIngestBuilder::finish` and
16460    /// raw `sheet_store_mut` access, are unsupported while a resumable
16461    /// recalculation is pending; they do not publish mutation revisions.
16462    pub fn evaluate_all_until(
16463        &mut self,
16464        stop: super::EvaluationStop,
16465    ) -> Result<super::EvaluationProgress, ExcelError> {
16466        self.ensure_not_poisoned()?;
16467        if self.has_active_recalc_job() {
16468            return Err(Self::active_recalc_job_error());
16469        }
16470        let report = self.drive_recalc_step(stop)?;
16471        Ok(match report.result {
16472            Some(result) => super::EvaluationProgress::Complete(result),
16473            None => super::EvaluationProgress::Incomplete(super::IncompleteEvaluation {
16474                reason: if report.phase == super::StepPhase::Cancelled {
16475                    super::StopReason::Cancelled
16476                } else {
16477                    super::StopReason::Deadline
16478                },
16479                computed_vertices: report.step_computed_vertices,
16480            }),
16481        })
16482    }
16483
16484    /// Whether a logical recalculation is awaiting another resumable call.
16485    pub fn has_pending_resumable_evaluation(&self) -> bool {
16486        self.resumable.is_some()
16487    }
16488
16489    /// Capture original values before a return or an acyclic installation can
16490    /// overwrite a member. Match the SCC task's overlay-first snapshot and its
16491    /// persistence repair for overlays cleared by structural edits.
16492    fn capture_original_cycle_seeds(
16493        &self,
16494        cycle: &[VertexId],
16495    ) -> FxHashMap<VertexId, LiteralValue> {
16496        cycle
16497            .iter()
16498            .map(|&vertex| {
16499                let value = if let Some(cell) = self.graph.get_cell_ref(vertex) {
16500                    let current = self
16501                        .read_cell_value(
16502                            self.graph.sheet_name(cell.sheet_id),
16503                            cell.coord.row() + 1,
16504                            cell.coord.col() + 1,
16505                        )
16506                        .unwrap_or(LiteralValue::Empty);
16507                    if matches!(current, LiteralValue::Empty) {
16508                        self.iterative_state_values
16509                            .get(&vertex)
16510                            .cloned()
16511                            .unwrap_or(current)
16512                    } else {
16513                        current
16514                    }
16515                } else {
16516                    self.graph.get_value(vertex).unwrap_or(LiteralValue::Empty)
16517                };
16518                (vertex, value)
16519            })
16520            .collect()
16521    }
16522
16523    fn record_original_cycle_seeds(&mut self, schedule: &super::scheduler::Schedule) {
16524        if self.resumable.as_ref().unwrap().original_cycles_recorded {
16525            return;
16526        }
16527        for cycle in &schedule.cycles {
16528            let mut key = cycle.clone();
16529            key.sort_unstable();
16530            let seeds = self.capture_original_cycle_seeds(&key);
16531            let state = self.resumable.as_mut().unwrap();
16532            state.original_cycles.insert(key.clone());
16533            // Retained SCCs were captured already at logical admission.
16534            state.cycle_seeds.entry(key).or_insert(seeds);
16535        }
16536        let state = self.resumable.as_mut().unwrap();
16537        state.original_cycles_recorded = true;
16538        state.refresh_seed_retained_bytes();
16539    }
16540
16541    fn resumable_should_stop(
16542        &self,
16543        units: usize,
16544        stop_at: Option<crate::instant::FzInstant>,
16545    ) -> bool {
16546        #[cfg(test)]
16547        if self
16548            .resumable_stop_after_units_for_test
16549            .is_some_and(|n| units >= n.max(1))
16550        {
16551            return true;
16552        }
16553        units > 0 && stop_at.is_some_and(|t| crate::instant::FzInstant::now() >= t)
16554    }
16555
16556    /// Evaluate all dirty/volatile vertices.
16557    pub fn evaluate_all(&mut self) -> Result<EvalResult, ExcelError> {
16558        self.ensure_not_poisoned()?;
16559        // `evaluate_all_unobserved` owns the `observe_function_semantic_epoch` guard.
16560        self.observe_evaluation_resource_request(EvaluationRequestKind::Full, |engine| {
16561            engine.evaluate_all_unobserved()
16562        })
16563    }
16564
16565    fn evaluate_all_unobserved(&mut self) -> Result<EvalResult, ExcelError> {
16566        debug_assert!(
16567            !self.graph.deferred_dirty_active(),
16568            "deferred-dirty scope leaked into evaluate_all: a begin_deferred_dirty \
16569             was not balanced by end_deferred_dirty"
16570        );
16571        self.observe_function_semantic_epoch()?;
16572        self.lookup_index_cache.reset_counters();
16573        let _source_cache = self.source_cache_session();
16574        self.validate_deterministic_mode()?;
16575        if self.config.defer_graph_building {
16576            // Build graph for all staged formulas before evaluating
16577            self.build_graph_all()?;
16578        }
16579        self.evaluate_all_coordinator()
16580    }
16581
16582    /// Coordinator for `evaluate_all`: starts the evaluation request and runs
16583    /// the per-cell pass.
16584    fn evaluate_all_coordinator(&mut self) -> Result<EvalResult, ExcelError> {
16585        self.require_unified_authority()?;
16586        self.begin_evaluation_request();
16587        self.evaluate_all_legacy_impl()
16588    }
16589
16590    /// Walk a schedule's units in condensation order: stamp each cyclic SCC
16591    /// at its position and evaluate each layer (parallel when enabled).
16592    ///
16593    /// Returns `(computed_vertices, cycle_count)` where `cycle_count` is the
16594    /// number of Cycle units walked (the former `schedule.cycles.len()`).
16595    fn legacy_pass_run_units(
16596        &mut self,
16597        schedule: &crate::engine::scheduler::Schedule,
16598    ) -> Result<(usize, usize), ExcelError> {
16599        let mut computed_vertices = 0;
16600        let mut cycle_count = 0;
16601        self.begin_pass(schedule);
16602        for (unit_index, &unit) in schedule.units.iter().enumerate() {
16603            match unit {
16604                ScheduleUnit::Cycle(i) => {
16605                    if self.handle_cycle_unit(schedule.unit_cycle(i), None, None, None)? > 0 {
16606                        cycle_count += 1;
16607                    }
16608                }
16609                ScheduleUnit::Layer(i) => {
16610                    let layer = schedule.unit_layer(i);
16611                    if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16612                        computed_vertices += self.evaluate_layer_parallel(layer)?;
16613                    } else {
16614                        computed_vertices += self.evaluate_layer_sequential(layer)?;
16615                    }
16616                }
16617            }
16618            if self.stop_after_unit(schedule, unit_index) {
16619                break;
16620            }
16621        }
16622        Ok((computed_vertices, cycle_count))
16623    }
16624
16625    /// Per-cell `evaluate_all` body, reached through the coordinator. This is
16626    /// an internal primitive; it must not be invoked directly from public APIs.
16627    ///
16628    /// Does NOT call `begin_evaluation_request` (cycle-telemetry reset +
16629    /// per-recalc clock sample): request begin happens at the public entry
16630    /// points / coordinators, so one request keeps one clock sample.
16631    fn evaluate_all_legacy_impl(&mut self) -> Result<EvalResult, ExcelError> {
16632        self.reset_virtual_dep_telemetry_if_disabled();
16633        let _span_eval =
16634            crate::engine::trace::fz_span!(tracing::Level::INFO, "evaluate", "evaluate.legacy");
16635        let start = crate::instant::FzInstant::now();
16636        let mut computed_vertices = 0;
16637        let mut cycle_errors = 0;
16638        let mut replan_iterations = 0;
16639        const MAX_REPLAN: usize = 5;
16640        let mut telemetry = self
16641            .config
16642            .enable_virtual_dep_telemetry
16643            .then(|| self.start_virtual_dep_telemetry());
16644
16645        loop {
16646            let to_evaluate = self.graph.get_evaluation_vertices();
16647            if to_evaluate.is_empty() {
16648                if let Some(t) = telemetry.as_mut()
16649                    && t.bailout_reason.is_none()
16650                {
16651                    t.bailout_reason = Some("no_work");
16652                }
16653                break;
16654            }
16655
16656            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
16657            if let Some(t) = telemetry.as_mut() {
16658                Self::accumulate_schedule_meta(t, &meta);
16659            }
16660
16661            let (pass_computed, pass_cycles) = self.legacy_pass_run_units(&schedule)?;
16662            computed_vertices += pass_computed;
16663            cycle_errors += pass_cycles;
16664
16665            // Check if dynamic dependencies changed
16666            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
16667            if let Some(t) = telemetry.as_mut() {
16668                t.changed_vdeps_total += changed_vertices.len();
16669            }
16670
16671            self.resource_checkpoint(0)?;
16672            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
16673                if let Some(t) = telemetry.as_mut() {
16674                    t.bailout_reason = Some("converged");
16675                }
16676                break;
16677            }
16678            if replan_iterations >= MAX_REPLAN {
16679                if let Some(mut t) = telemetry.take() {
16680                    t.bailout_reason = Some("max_replan");
16681                    t.replan_iterations = replan_iterations;
16682                    self.last_virtual_dep_telemetry = t;
16683                }
16684                return Err(
16685                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
16686                );
16687            }
16688
16689            replan_iterations += 1;
16690        }
16691
16692        if let Some(mut t) = telemetry {
16693            t.replan_iterations = replan_iterations;
16694            self.last_virtual_dep_telemetry = t;
16695        }
16696
16697        // Re-dirty volatile vertices for the next evaluation cycle
16698        self.redirty_for_next_recalc();
16699
16700        // Advance recalc epoch after a full evaluation pass finishes
16701        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
16702
16703        Ok(EvalResult {
16704            computed_vertices,
16705            cycle_errors,
16706            elapsed: start.elapsed(),
16707        })
16708    }
16709
16710    pub fn evaluate_all_with_target_delta(
16711        &mut self,
16712    ) -> Result<(EvalResult, crate::engine::TargetEvalDelta), ExcelError> {
16713        self.ensure_not_poisoned()?;
16714        self.observe_evaluation_resource_request(EvaluationRequestKind::FullWithDelta, |engine| {
16715            engine.observe_function_semantic_epoch()?;
16716            let mut collector = DeltaCollector::new(DeltaMode::Cells);
16717            let result = engine.evaluate_all_with_delta_collector(&mut collector)?;
16718            Ok((result, collector.finish_target()))
16719        })
16720    }
16721
16722    pub fn evaluate_all_with_delta(&mut self) -> Result<(EvalResult, EvalDelta), ExcelError> {
16723        self.ensure_not_poisoned()?;
16724        self.evaluate_all_with_delta_policy(EvalDeltaCompatibilityPolicy::Unlimited)
16725    }
16726
16727    pub fn evaluate_all_with_delta_policy(
16728        &mut self,
16729        policy: EvalDeltaCompatibilityPolicy,
16730    ) -> Result<(EvalResult, EvalDelta), ExcelError> {
16731        self.ensure_not_poisoned()?;
16732        self.observe_evaluation_resource_request(EvaluationRequestKind::FullWithDelta, |engine| {
16733            engine.observe_function_semantic_epoch()?;
16734            let mut collector = DeltaCollector::new(DeltaMode::Cells);
16735            let result = engine.evaluate_all_with_delta_collector(&mut collector)?;
16736            Ok((result, collector.finish_with_policy(policy)?))
16737        })
16738    }
16739
16740    fn evaluate_all_with_delta_collector(
16741        &mut self,
16742        delta: &mut DeltaCollector,
16743    ) -> Result<EvalResult, ExcelError> {
16744        let _source_cache = self.source_cache_session();
16745        if self.config.defer_graph_building {
16746            self.build_graph_all()?;
16747        }
16748        self.require_unified_authority()?;
16749        self.begin_evaluation_request();
16750        self.reset_virtual_dep_telemetry_if_disabled();
16751        let _span_eval = crate::engine::trace::fz_span!(
16752            tracing::Level::INFO,
16753            "evaluate",
16754            "evaluate.legacy_delta"
16755        );
16756        let start = crate::instant::FzInstant::now();
16757        let mut computed_vertices = 0;
16758        let mut cycle_errors = 0;
16759
16760        let mut replan_iterations = 0;
16761        const MAX_REPLAN: usize = 5;
16762        let mut telemetry = self
16763            .config
16764            .enable_virtual_dep_telemetry
16765            .then(|| self.start_virtual_dep_telemetry());
16766
16767        loop {
16768            let to_evaluate = self.graph.get_evaluation_vertices();
16769            if to_evaluate.is_empty() {
16770                if let Some(t) = telemetry.as_mut()
16771                    && t.bailout_reason.is_none()
16772                {
16773                    t.bailout_reason = Some("no_work");
16774                }
16775                break;
16776            }
16777
16778            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
16779            if let Some(t) = telemetry.as_mut() {
16780                Self::accumulate_schedule_meta(t, &meta);
16781            }
16782
16783            self.begin_pass(&schedule);
16784            for (unit_index, &unit) in schedule.units.iter().enumerate() {
16785                match unit {
16786                    ScheduleUnit::Cycle(i) => {
16787                        if self.handle_cycle_unit(
16788                            schedule.unit_cycle(i),
16789                            Some(delta),
16790                            None,
16791                            None,
16792                        )? > 0
16793                        {
16794                            cycle_errors += 1;
16795                        }
16796                    }
16797                    ScheduleUnit::Layer(i) => {
16798                        let layer = schedule.unit_layer(i);
16799                        if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16800                            computed_vertices +=
16801                                self.evaluate_layer_parallel_with_delta(layer, delta)?;
16802                        } else {
16803                            computed_vertices +=
16804                                self.evaluate_layer_sequential_with_delta(layer, delta)?;
16805                        }
16806                    }
16807                }
16808                if self.stop_after_unit(&schedule, unit_index) {
16809                    break;
16810                }
16811            }
16812
16813            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
16814            if let Some(t) = telemetry.as_mut() {
16815                t.changed_vdeps_total += changed_vertices.len();
16816            }
16817            self.resource_checkpoint(0)?;
16818            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
16819                if let Some(t) = telemetry.as_mut() {
16820                    t.bailout_reason = Some("converged");
16821                }
16822                break;
16823            }
16824            if replan_iterations >= MAX_REPLAN {
16825                if let Some(mut t) = telemetry.take() {
16826                    t.bailout_reason = Some("max_replan");
16827                    t.replan_iterations = replan_iterations;
16828                    self.last_virtual_dep_telemetry = t;
16829                }
16830                return Err(
16831                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
16832                );
16833            }
16834            replan_iterations += 1;
16835        }
16836
16837        if let Some(mut t) = telemetry {
16838            t.replan_iterations = replan_iterations;
16839            self.last_virtual_dep_telemetry = t;
16840        }
16841
16842        self.redirty_for_next_recalc();
16843        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
16844
16845        Ok(EvalResult {
16846            computed_vertices,
16847            cycle_errors,
16848            elapsed: start.elapsed(),
16849        })
16850    }
16851
16852    /// Convenience: demand-driven evaluation of a single cell by sheet name and row/col.
16853    ///
16854    /// This will evaluate only the minimal set of dirty / volatile precedents required
16855    /// to bring the target cell up-to-date (as if a user asked for that single value),
16856    /// rather than scheduling a full workbook recalc. If the cell is already clean and
16857    /// non-volatile, no vertices will be recomputed.
16858    ///
16859    /// Returns the (possibly newly computed) value stored for the cell afterwards.
16860    /// Empty cells return None. Errors are surfaced via the Result type.
16861    pub fn evaluate_cell(
16862        &mut self,
16863        sheet: &str,
16864        row: u32,
16865        col: u32,
16866    ) -> Result<Option<LiteralValue>, ExcelError> {
16867        self.ensure_not_poisoned()?;
16868        self.observe_evaluation_resource_request(EvaluationRequestKind::Cell, |engine| {
16869            engine.evaluate_cell_unobserved(sheet, row, col)
16870        })
16871    }
16872
16873    fn evaluate_cell_unobserved(
16874        &mut self,
16875        sheet: &str,
16876        row: u32,
16877        col: u32,
16878    ) -> Result<Option<LiteralValue>, ExcelError> {
16879        if row == 0 || col == 0 {
16880            return Err(ExcelError::new(ExcelErrorKind::Ref)
16881                .with_message("Row and column must be >= 1".to_string()));
16882        }
16883
16884        let result = self.evaluate_cells(&[(sheet, row, col)])?;
16885
16886        match result.len() {
16887            0 => Ok(None),
16888            1 => {
16889                let v = result.into_iter().next().unwrap();
16890                Ok(v)
16891            }
16892            _ => unreachable!("evaluate_cells returned unexpected length"),
16893        }
16894    }
16895
16896    /// Convenience: demand-driven evaluation of multiple cells; accepts a slice of
16897    /// (sheet, row, col) triples. The union of required dirty / volatile precedents
16898    /// is computed once and evaluated, which is typically faster than calling
16899    /// `evaluate_cell` repeatedly for a related set of targets.
16900    ///
16901    /// Returns the resulting values for each requested target in the same order.
16902    pub fn evaluate_cells(
16903        &mut self,
16904        targets: &[(&str, u32, u32)],
16905    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
16906        self.ensure_not_poisoned()?;
16907        self.observe_evaluation_resource_request(EvaluationRequestKind::Cells, |engine| {
16908            engine.evaluate_cells_unobserved(targets)
16909        })
16910    }
16911
16912    fn evaluate_cells_unobserved(
16913        &mut self,
16914        targets: &[(&str, u32, u32)],
16915    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
16916        self.observe_function_semantic_epoch()?;
16917        debug_assert!(
16918            !self.graph.deferred_dirty_active(),
16919            "deferred-dirty scope leaked into evaluate_cells: a begin_deferred_dirty \
16920             was not balanced by end_deferred_dirty"
16921        );
16922        self.validate_deterministic_mode()?;
16923        if targets.is_empty() {
16924            return Ok(Vec::new());
16925        }
16926        let typed_targets = self.legacy_coordinate_targets(targets);
16927        self.evaluate_mixed_targets(&typed_targets, None)?;
16928        Ok(targets
16929            .iter()
16930            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
16931            .collect())
16932    }
16933
16934    pub fn evaluate_cells_cancellable(
16935        &mut self,
16936        targets: &[(&str, u32, u32)],
16937        cancel: crate::engine::CancelToken,
16938    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
16939        self.ensure_not_poisoned()?;
16940        self.observe_evaluation_resource_request(
16941            EvaluationRequestKind::CellsCancellable,
16942            |engine| {
16943                engine.observe_function_semantic_epoch()?;
16944                engine.active_cancel_flag = Some(cancel.clone());
16945                let res = engine.evaluate_cells_cancellable_impl(targets, cancel.as_flag());
16946                engine.active_cancel_flag = None;
16947                res
16948            },
16949        )
16950    }
16951
16952    fn evaluate_cells_cancellable_impl(
16953        &mut self,
16954        targets: &[(&str, u32, u32)],
16955        cancel_flag: &AtomicBool,
16956    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
16957        self.validate_deterministic_mode()?;
16958        if targets.is_empty() {
16959            return Ok(Vec::new());
16960        }
16961        if cancel_flag.load(Ordering::Relaxed) {
16962            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16963                .with_message("Evaluation cancelled before target preparation"));
16964        }
16965        let typed_targets = self.legacy_coordinate_targets(targets);
16966        self.evaluate_mixed_targets(&typed_targets, None)?;
16967        Ok(targets
16968            .iter()
16969            .map(|(sheet, row, col)| self.get_cell_value(sheet, *row, *col))
16970            .collect())
16971    }
16972
16973    pub fn evaluate_cells_with_target_delta(
16974        &mut self,
16975        targets: &[(&str, u32, u32)],
16976    ) -> Result<(Vec<Option<LiteralValue>>, crate::engine::TargetEvalDelta), ExcelError> {
16977        self.ensure_not_poisoned()?;
16978        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
16979            engine.observe_function_semantic_epoch()?;
16980            engine.validate_deterministic_mode()?;
16981            if targets.is_empty() {
16982                return Ok((Vec::new(), crate::engine::TargetEvalDelta::default()));
16983            }
16984            let mut collector = DeltaCollector::new(DeltaMode::Cells);
16985            engine.evaluate_until_with_delta_collector(targets, &mut collector)?;
16986            let values = targets
16987                .iter()
16988                .map(|(sheet, row, col)| engine.get_cell_value(sheet, *row, *col))
16989                .collect();
16990            Ok((values, collector.finish_target()))
16991        })
16992    }
16993
16994    pub fn evaluate_cells_with_delta(
16995        &mut self,
16996        targets: &[(&str, u32, u32)],
16997    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
16998        self.ensure_not_poisoned()?;
16999        self.evaluate_cells_with_delta_policy(targets, EvalDeltaCompatibilityPolicy::Unlimited)
17000    }
17001
17002    pub fn evaluate_cells_with_delta_policy(
17003        &mut self,
17004        targets: &[(&str, u32, u32)],
17005        policy: EvalDeltaCompatibilityPolicy,
17006    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
17007        self.ensure_not_poisoned()?;
17008        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
17009            engine.evaluate_cells_with_delta_unobserved(targets, policy)
17010        })
17011    }
17012
17013    fn evaluate_cells_with_delta_unobserved(
17014        &mut self,
17015        targets: &[(&str, u32, u32)],
17016        policy: EvalDeltaCompatibilityPolicy,
17017    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
17018        self.observe_function_semantic_epoch()?;
17019        self.validate_deterministic_mode()?;
17020        if targets.is_empty() {
17021            return Ok((Vec::new(), EvalDelta::default()));
17022        }
17023        let mut collector = DeltaCollector::new(DeltaMode::Cells);
17024        self.evaluate_until_with_delta_collector(targets, &mut collector)?;
17025        let values = targets
17026            .iter()
17027            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
17028            .collect();
17029        Ok((values, collector.finish_with_policy(policy)?))
17030    }
17031
17032    /// Get the evaluation plan for target cells without actually evaluating them
17033    pub fn get_eval_plan(&self, targets: &[(&str, u32, u32)]) -> Result<EvalPlan, ExcelError> {
17034        self.ensure_not_poisoned()?;
17035        if targets.is_empty() {
17036            return Ok(EvalPlan {
17037                total_vertices_to_evaluate: 0,
17038                layers: Vec::new(),
17039                cycles_detected: 0,
17040                dirty_count: 0,
17041                volatile_count: 0,
17042                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
17043                estimated_parallel_layers: 0,
17044                target_cells: Vec::new(),
17045            });
17046        }
17047        if self.config.defer_graph_building && self.has_staged_formulas() {
17048            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
17049                "Evaluation plan requested with deferred graph; build first or call evaluate_*",
17050            ));
17051        }
17052
17053        // Convert targets to A1 notation for consistency
17054        let addresses: Vec<String> = targets
17055            .iter()
17056            .map(|(s, r, c)| format!("{}!{}{}", s, Self::col_to_letters(*c), r))
17057            .collect();
17058
17059        // Parse target cell addresses
17060        let mut target_addrs = Vec::new();
17061        for (sheet, row, col) in targets {
17062            if let Some(sheet_id) = self.graph.sheet_id(sheet) {
17063                let coord = Coord::from_excel(*row, *col, true, true);
17064                target_addrs.push(CellRef::new(sheet_id, coord));
17065            }
17066        }
17067
17068        // Find vertex IDs for targets
17069        let mut target_vertex_ids = Vec::new();
17070        for addr in &target_addrs {
17071            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
17072                target_vertex_ids.push(vertex_id);
17073            }
17074        }
17075
17076        if target_vertex_ids.is_empty() {
17077            return Ok(EvalPlan {
17078                total_vertices_to_evaluate: 0,
17079                layers: Vec::new(),
17080                cycles_detected: 0,
17081                dirty_count: 0,
17082                volatile_count: 0,
17083                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
17084                estimated_parallel_layers: 0,
17085                target_cells: addresses,
17086            });
17087        }
17088
17089        // Build demand subgraph with virtual edges (same as evaluate_until)
17090        let (precedents_to_eval, vdeps) = self.demand_subgraph(&target_vertex_ids)?;
17091
17092        if precedents_to_eval.is_empty() {
17093            return Ok(EvalPlan {
17094                total_vertices_to_evaluate: 0,
17095                layers: Vec::new(),
17096                cycles_detected: 0,
17097                dirty_count: 0,
17098                volatile_count: 0,
17099                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
17100                estimated_parallel_layers: 0,
17101                target_cells: addresses,
17102            });
17103        }
17104
17105        // Count dirty and volatile vertices
17106        let mut dirty_count = 0;
17107        let mut volatile_count = 0;
17108        for &vertex_id in &precedents_to_eval {
17109            if self.graph.is_dirty(vertex_id) {
17110                dirty_count += 1;
17111            }
17112            if self.graph.is_volatile(vertex_id) {
17113                volatile_count += 1;
17114            }
17115        }
17116
17117        // Create schedule for the minimal subgraph honoring virtual edges
17118        let schedule = self.create_authority_schedule(&precedents_to_eval, &vdeps, None)?;
17119
17120        // Build layer information
17121        let mut layers = Vec::new();
17122        let mut estimated_parallel_layers = 0;
17123        let parallel_enabled = self.config.enable_parallel && self.thread_pool.is_some();
17124
17125        for layer in &schedule.layers {
17126            let parallel_eligible = parallel_enabled && layer.vertices.len() > 1;
17127            if parallel_eligible {
17128                estimated_parallel_layers += 1;
17129            }
17130
17131            // Get sample cell addresses (up to 5)
17132            let sample_cells: Vec<String> = layer
17133                .vertices
17134                .iter()
17135                .take(5)
17136                .filter_map(|&vertex_id| {
17137                    self.graph
17138                        .get_cell_ref_for_vertex(vertex_id)
17139                        .map(|cell_ref| {
17140                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
17141                            format!(
17142                                "{}!{}{}",
17143                                sheet_name,
17144                                Self::col_to_letters(cell_ref.coord.col().saturating_add(1)),
17145                                cell_ref.coord.row() + 1
17146                            )
17147                        })
17148                })
17149                .collect();
17150
17151            layers.push(LayerInfo {
17152                vertex_count: layer.vertices.len(),
17153                parallel_eligible,
17154                sample_cells,
17155            });
17156        }
17157
17158        Ok(EvalPlan {
17159            total_vertices_to_evaluate: precedents_to_eval.len(),
17160            layers,
17161            cycles_detected: schedule.cycles.len(),
17162            dirty_count,
17163            volatile_count,
17164            parallel_enabled,
17165            estimated_parallel_layers,
17166            target_cells: addresses,
17167        })
17168    }
17169    /// Helper to create a schedule, integrating virtual dependencies automatically.
17170    fn create_evaluation_schedule(
17171        &mut self,
17172        to_evaluate: &[VertexId],
17173    ) -> Result<EvaluationScheduleBuildOutput, ExcelError> {
17174        #[cfg(any(test, feature = "benchmark_internal"))]
17175        {
17176            self.recalc_reuse_probe.get_mut().unwrap().schedule_requests += 1;
17177        }
17178        // Fold pending edge deltas once per schedule build so traversal uses
17179        // the zero-allocation CSR slices (#125).
17180        #[cfg(any(test, feature = "legacy_oracle"))]
17181        self.graph.flush_pending_edge_deltas();
17182        // The cache key includes the authority revision: sync first.
17183        self.graph.authority_sync();
17184        if self.can_use_static_schedule_cache(to_evaluate) {
17185            // A recent schedule for the same request becomes the current one.
17186            let revision = self.schedule_cache_authority_revision();
17187            let current = |e: &CachedScheduleEntry| {
17188                e.topology_epoch == self.topology_epoch && e.authority_revision == revision
17189            };
17190            if !self
17191                .cached_static_schedule
17192                .as_ref()
17193                .is_some_and(|c| current(c) && c.candidate_vertices.equals(to_evaluate))
17194                && let Some(i) = self.recent_schedules.iter().position(|e| {
17195                    current(e)
17196                        && e.candidate_vertices.len() == to_evaluate.len()
17197                        && e.candidate_vertices.equals(to_evaluate)
17198                })
17199            {
17200                let hit = self.recent_schedules.remove(i);
17201                if let Some(previous) = self.cached_static_schedule.replace(hit) {
17202                    self.retain_recent_schedule(previous);
17203                }
17204            }
17205            if let Some(cached) = self.cached_static_schedule.as_ref()
17206                && cached.topology_epoch == self.topology_epoch
17207                && cached.authority_revision == self.schedule_cache_authority_revision()
17208                && cached.candidate_vertices.equals(to_evaluate)
17209            {
17210                let meta = ScheduleBuildMeta {
17211                    candidate_vertices: to_evaluate.len(),
17212                    vdeps_vertices: 0,
17213                    vdeps_edges: 0,
17214                    builder_elapsed_ms: 0,
17215                    used_virtual_schedule: false,
17216                    schedule_cache_hit: true,
17217                    schedule_cache_eligible: true,
17218                };
17219                #[cfg(any(test, feature = "benchmark_internal"))]
17220                {
17221                    let mut probe = self.recalc_reuse_probe.lock().unwrap();
17222                    probe.schedule_cache_hits += 1;
17223                    probe.schedule_shared_handles += 1;
17224                }
17225                return Ok((
17226                    EvaluationSchedule::Shared(Arc::clone(&cached.schedule)),
17227                    FxHashMap::default(),
17228                    meta,
17229                ));
17230            }
17231
17232            let (schedule, vdeps, mut meta) = match self.schedule_from_base(to_evaluate)? {
17233                Some(schedule) => (
17234                    schedule,
17235                    FxHashMap::default(),
17236                    ScheduleBuildMeta {
17237                        candidate_vertices: to_evaluate.len(),
17238                        vdeps_vertices: 0,
17239                        vdeps_edges: 0,
17240                        builder_elapsed_ms: 0,
17241                        used_virtual_schedule: false,
17242                        schedule_cache_hit: false,
17243                        schedule_cache_eligible: true,
17244                    },
17245                ),
17246                None => self.create_evaluation_schedule_active(to_evaluate)?,
17247            };
17248            meta.schedule_cache_hit = false;
17249            meta.schedule_cache_eligible = true;
17250            #[cfg(any(test, feature = "benchmark_internal"))]
17251            {
17252                self.recalc_reuse_probe
17253                    .get_mut()
17254                    .unwrap()
17255                    .schedule_cache_misses += 1;
17256            }
17257            let schedule = if vdeps.is_empty() {
17258                // Clone previously discarded builder spare capacity. Keep that compact
17259                // retained payload while sharing it with the current request.
17260                let mut schedule = schedule;
17261                schedule.units.shrink_to_fit();
17262                for layer in &mut schedule.layers {
17263                    layer.vertices.shrink_to_fit();
17264                }
17265                schedule.layers.shrink_to_fit();
17266                for cycle in &mut schedule.cycles {
17267                    cycle.shrink_to_fit();
17268                }
17269                schedule.cycles.shrink_to_fit();
17270                let schedule = Arc::new(schedule);
17271                #[cfg(any(test, feature = "benchmark_internal"))]
17272                {
17273                    self.recalc_reuse_probe
17274                        .get_mut()
17275                        .unwrap()
17276                        .schedule_shared_handles += 1;
17277                }
17278                let entry = CachedScheduleEntry {
17279                    topology_epoch: self.topology_epoch,
17280                    authority_revision: self.schedule_cache_authority_revision(),
17281                    candidate_vertices: VertexIdRuns::from_slice(to_evaluate),
17282                    schedule: Arc::clone(&schedule),
17283                };
17284                if let Some(previous) = self.cached_static_schedule.replace(entry) {
17285                    self.retain_recent_schedule(previous);
17286                }
17287                EvaluationSchedule::Shared(schedule)
17288            } else {
17289                EvaluationSchedule::Owned(schedule)
17290            };
17291            return Ok((schedule, vdeps, meta));
17292        }
17293
17294        let (schedule, vdeps, mut meta) = self.create_evaluation_schedule_active(to_evaluate)?;
17295        meta.schedule_cache_hit = false;
17296        meta.schedule_cache_eligible = false;
17297        #[cfg(any(test, feature = "benchmark_internal"))]
17298        {
17299            self.recalc_reuse_probe
17300                .get_mut()
17301                .unwrap()
17302                .schedule_cache_ineligible += 1;
17303        }
17304        Ok((EvaluationSchedule::Owned(schedule), vdeps, meta))
17305    }
17306
17307    /// Plan reuse: the base schedule restricted to `to_evaluate` when it
17308    /// is current, covers the request, has at least
17309    /// `BASE_SCHEDULE_MIN_REQUEST` candidates, and is at most
17310    /// `BASE_SCHEDULE_RATIO` times its size (restricting walks the whole
17311    /// base; planning costs far more per candidate).
17312    fn schedule_from_base(
17313        &mut self,
17314        to_evaluate: &[VertexId],
17315    ) -> Result<Option<crate::engine::scheduler::Schedule>, ExcelError> {
17316        let revision = self.schedule_cache_authority_revision();
17317        let current = |e: &&CachedScheduleEntry| {
17318            e.topology_epoch == self.topology_epoch && e.authority_revision == revision
17319        };
17320        // The base, or the current schedule when larger (the first
17321        // evaluation's, before a later request replaces it).
17322        let Some(base) = [
17323            self.base_schedule.as_ref(),
17324            self.cached_static_schedule.as_ref(),
17325        ]
17326        .into_iter()
17327        .flatten()
17328        .filter(current)
17329        .max_by_key(|e| e.candidate_vertices.len()) else {
17330            return Ok(None);
17331        };
17332        let base_len = base.candidate_vertices.len();
17333        if base_len <= BASE_SCHEDULE_MIN_VERTICES
17334            || to_evaluate.len() < BASE_SCHEDULE_MIN_REQUEST
17335            || to_evaluate.len() > base_len
17336            || base_len > to_evaluate.len().saturating_mul(BASE_SCHEDULE_RATIO)
17337        {
17338            return Ok(None);
17339        }
17340        let mut keep = crate::engine::idset::DenseIdSet::default();
17341        keep.extend(to_evaluate.iter().copied());
17342        let Some((schedule, kept)) = base.schedule.restrict(&keep) else {
17343            return Ok(None);
17344        };
17345        // Every requested vertex must be in the base.
17346        if kept != keep.len() {
17347            return Ok(None);
17348        }
17349        if let Some(ledger) = self.active_resource_ledger.as_mut() {
17350            let layers: usize = schedule
17351                .layers
17352                .iter()
17353                .map(|l| {
17354                    l.vertices.capacity() * std::mem::size_of::<VertexId>()
17355                        + l.runs.capacity()
17356                            * std::mem::size_of::<crate::engine::scheduler::LayerRun>()
17357                })
17358                .sum();
17359            let bytes = (keep.heap_bytes()
17360                + layers
17361                + schedule.layers.capacity()
17362                    * std::mem::size_of::<crate::engine::scheduler::Layer>()
17363                + schedule.units.capacity()
17364                    * std::mem::size_of::<crate::engine::scheduler::ScheduleUnit>()
17365                + schedule
17366                    .cycles
17367                    .iter()
17368                    .map(|c| c.capacity() * 4)
17369                    .sum::<usize>()) as u64;
17370            ledger
17371                .reserve_schedule_discovery(bytes)
17372                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17373            ledger
17374                .release_scratch(bytes)
17375                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17376        }
17377        #[cfg(debug_assertions)]
17378        self.debug_check_restricted_schedule(to_evaluate, &schedule);
17379        #[cfg(any(test, feature = "benchmark_internal"))]
17380        {
17381            self.recalc_reuse_probe
17382                .get_mut()
17383                .unwrap()
17384                .schedule_base_restrictions += 1;
17385        }
17386        Ok(Some(schedule))
17387    }
17388
17389    /// Debug builds: a restricted schedule holds each requested vertex
17390    /// once and orders the request like a freshly planned schedule (every
17391    /// dependency among the request in an earlier unit).
17392    #[cfg(debug_assertions)]
17393    fn debug_check_restricted_schedule(
17394        &self,
17395        to_evaluate: &[VertexId],
17396        schedule: &crate::engine::scheduler::Schedule,
17397    ) {
17398        let mut position: FxHashMap<VertexId, usize> = FxHashMap::default();
17399        for (u, unit) in schedule.units.iter().enumerate() {
17400            let vertices: &[VertexId] = match *unit {
17401                crate::engine::scheduler::ScheduleUnit::Layer(i) => {
17402                    &schedule.unit_layer(i).vertices
17403                }
17404                crate::engine::scheduler::ScheduleUnit::Cycle(i) => schedule.unit_cycle(i),
17405            };
17406            for &v in vertices {
17407                assert!(
17408                    position.insert(v, u).is_none(),
17409                    "vertex twice in a restricted schedule"
17410                );
17411            }
17412        }
17413        assert_eq!(position.len(), to_evaluate.len());
17414        if to_evaluate.len() > 512 {
17415            return;
17416        }
17417        // Every arc among the request (the store's edge images) goes to an
17418        // earlier unit, or within one sequential layer (a chain) or cycle.
17419        let Ok(store) = self.graph.authority_plan_store() else {
17420            return;
17421        };
17422        let cells: Vec<(VertexId, (u16, u32, u32))> = to_evaluate
17423            .iter()
17424            .filter_map(|&v| self.graph.authority_cell_of_vertex(v).map(|c| (v, c)))
17425            .collect();
17426        for &(v, (sheet, row, col)) in &cells {
17427            let Some(owner) = store.owner_at((sheet, row, col)) else {
17428                continue;
17429            };
17430            let Ok(refined) = store.refine_owner_column(owner, col, row, row, None) else {
17431                continue;
17432            };
17433            for piece in &refined.pieces {
17434                for edge in &refined.edges[piece.edge_start..piece.edge_end] {
17435                    let Some(image) = edge.proj.forward(&piece.domain) else {
17436                        continue;
17437                    };
17438                    for &(d, (s2, r2, c2)) in &cells {
17439                        if d == v
17440                            || edge.proj.sheet != s2
17441                            || !(image.r0 <= r2
17442                                && r2 <= image.r1
17443                                && image.c0 <= c2
17444                                && c2 <= image.c1)
17445                        {
17446                            continue;
17447                        }
17448                        let (pd, pv) = (position[&d], position[&v]);
17449                        let same_ok = pd == pv
17450                            && match schedule.units[pv] {
17451                                crate::engine::scheduler::ScheduleUnit::Layer(i) => {
17452                                    schedule.unit_layer(i).sequential
17453                                }
17454                                crate::engine::scheduler::ScheduleUnit::Cycle(_) => true,
17455                            };
17456                        assert!(
17457                            pd < pv || same_ok,
17458                            "restricted schedule orders {v:?} before its precedent {d:?}"
17459                        );
17460                    }
17461                }
17462            }
17463        }
17464    }
17465
17466    /// Compress family formulas once per authority build (see
17467    /// `EvalConfig::formula_compression`), when no staged or deferred
17468    /// formula package can hold arena ids.
17469    fn maybe_compress_formulas(&mut self) {
17470        if !self.config.formula_compression {
17471            return;
17472        }
17473        let builds = self.graph.authority_host().builds;
17474        if self.compressed_at_build == Some(builds) {
17475            return;
17476        }
17477        self.compressed_at_build = Some(builds);
17478        if self.has_staged_formulas() {
17479            // Staged packages hold arena ids: no compaction. Members that
17480            // are already compressed can still leave the per-cell maps.
17481            self.graph.virtualize_family_members();
17482            return;
17483        }
17484        let pool = self.thread_pool.clone();
17485        let (_, garbage) = self.graph.compress_family_formulas(pool.as_deref());
17486        self.graph.virtualize_family_members();
17487        // Freeing the dropped members' reference texts (one allocation each)
17488        // is most of compaction; with a pool it happens off the critical
17489        // path.
17490        match pool {
17491            Some(pool) if garbage.len() >= 1024 => pool.spawn(move || drop(garbage)),
17492            _ => drop(garbage),
17493        }
17494    }
17495
17496    fn create_evaluation_schedule_active(
17497        &mut self,
17498        to_evaluate: &[VertexId],
17499    ) -> Result<ScheduleBuildOutput, ExcelError> {
17500        self.graph.authority_sync();
17501        self.maybe_compress_formulas();
17502        let mut ledger = self.active_resource_ledger.take();
17503        let result = self.create_evaluation_schedule_uncached(to_evaluate, ledger.as_mut());
17504        self.active_resource_ledger = ledger;
17505        result
17506    }
17507
17508    fn create_evaluation_schedule_uncached(
17509        &self,
17510        to_evaluate: &[VertexId],
17511        #[allow(unused_variables)] ledger: Option<&mut ResourceLedger>,
17512    ) -> Result<ScheduleBuildOutput, ExcelError> {
17513        #[cfg(any(test, feature = "benchmark_internal"))]
17514        {
17515            self.recalc_reuse_probe.lock().unwrap().schedule_builds += 1;
17516        }
17517        let builder = VirtualDepBuilder::new(self);
17518        #[allow(unused_mut)]
17519        let (mut vdeps, augmented, builder_elapsed_ms, vdeps_edges) =
17520            if self.config.enable_virtual_dep_telemetry {
17521                let build_started = crate::instant::FzInstant::now();
17522                let (vdeps, augmented) = builder.build(to_evaluate);
17523                let builder_elapsed_ms = build_started.elapsed().as_millis();
17524                let vdeps_edges = vdeps.values().map(|deps| deps.len()).sum::<usize>();
17525                (vdeps, augmented, builder_elapsed_ms, vdeps_edges)
17526            } else {
17527                let (vdeps, augmented) = builder.build(to_evaluate);
17528                (vdeps, augmented, 0, 0)
17529            };
17530
17531        // Replan hints from stale dynamic reads earlier in this request.
17532        {
17533            self.freshness_merge_hints(to_evaluate, &mut vdeps);
17534            self.freshness_extent_hints(to_evaluate, &mut vdeps);
17535        }
17536        let mut final_evaluate = to_evaluate.to_vec();
17537        if !augmented.is_empty() {
17538            final_evaluate.extend(augmented);
17539            final_evaluate.sort_unstable();
17540            final_evaluate.dedup();
17541        }
17542
17543        let use_virtual = !vdeps.is_empty();
17544
17545        let schedule = self.create_authority_schedule(&final_evaluate, &vdeps, ledger)?;
17546
17547        let meta = ScheduleBuildMeta {
17548            candidate_vertices: to_evaluate.len(),
17549            vdeps_vertices: vdeps.len(),
17550            vdeps_edges,
17551            builder_elapsed_ms,
17552            used_virtual_schedule: use_virtual,
17553            schedule_cache_hit: false,
17554            schedule_cache_eligible: false,
17555        };
17556
17557        Ok((schedule, vdeps, meta))
17558    }
17559
17560    fn create_authority_schedule(
17561        &self,
17562        candidates: &[VertexId],
17563        vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
17564        mut ledger: Option<&mut ResourceLedger>,
17565    ) -> Result<crate::engine::scheduler::Schedule, ExcelError> {
17566        use crate::engine::authority::{
17567            geom::{Cover, Rect},
17568            plan_schedule, planner,
17569            proj::{AxisMap, RefProj},
17570            store::{EdgeKey, Tag},
17571        };
17572        let failure = |message: String| {
17573            ExcelError::new(ExcelErrorKind::Error)
17574                .with_message(format!("unified_authority planner: {message}"))
17575        };
17576        self.cancellation_checkpoint("Evaluation cancelled before authority planning")?;
17577        let store = self
17578            .graph
17579            .authority_plan_store()
17580            .map_err(Self::authority_excel_error)?;
17581        // One formula cell without hints (a tiny edit): its plan is the cell
17582        // alone unless it reads itself (`planner::plan_single`).
17583        if let [only] = candidates
17584            && vdeps.is_empty()
17585            && self.graph.authority_host().observed(*only).is_none()
17586            && let Some(cell) = self.graph.authority_cell_of_vertex(*only)
17587            && cell.0 != crate::engine::authority::geom::SYMBOL_SHEET
17588            && let Some(single) = planner::plan_single(store, cell)
17589        {
17590            #[cfg(debug_assertions)]
17591            {
17592                let mut cover = Cover::new();
17593                cover.insert_rect(cell.0, &Rect::new(cell.1, cell.2, cell.1, cell.2));
17594                let general = planner::plan_with_hints(store, &cover, &[], None, None, None)
17595                    .expect("general plan of one cell");
17596                assert_eq!(
17597                    general.cells.as_slice(),
17598                    &[single],
17599                    "single-cell plan differs from the planner at {cell:?}"
17600                );
17601            }
17602            let adapted = plan_schedule::schedule(
17603                &[single],
17604                0,
17605                None,
17606                |cell| {
17607                    self.graph
17608                        .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
17609                        .ok_or_else(|| failure("missing executor identity".to_owned()))
17610                },
17611                |_work| Ok(()),
17612            )
17613            .map_err(|error| match error {
17614                plan_schedule::ScheduleError::Runtime(error) => error,
17615                other => failure(format!("{other:?}")),
17616            })?;
17617            if let Some(ledger) = ledger {
17618                ledger
17619                    .reserve_schedule_discovery(adapted.peak_heap_bytes)
17620                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17621                ledger
17622                    .release_scratch(adapted.peak_heap_bytes)
17623                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17624            }
17625            return Ok(adapted.schedule);
17626        }
17627        // A small request of grid formula cells without hints: per-cell
17628        // arcs and longest-path levels (`planner::plan_small`).
17629        if (2..=planner::SMALL_PLAN_MAX).contains(&candidates.len())
17630            && vdeps.is_empty()
17631            && candidates
17632                .iter()
17633                .all(|&v| self.graph.authority_host().observed(v).is_none())
17634        {
17635            let cells: Option<Vec<(u16, u32, u32)>> = candidates
17636                .iter()
17637                .map(|&v| self.graph.authority_cell_of_vertex(v))
17638                .collect();
17639            if let Some(small) = cells.as_deref().and_then(|c| planner::plan_small(store, c)) {
17640                #[cfg(debug_assertions)]
17641                {
17642                    let mut cover = Cover::new();
17643                    for c in cells.as_deref().unwrap_or_default() {
17644                        cover.insert_rect(c.0, &Rect::new(c.1, c.2, c.1, c.2));
17645                    }
17646                    let general = planner::plan_with_hints(store, &cover, &[], None, None, None)
17647                        .expect("general plan of a small request");
17648                    let key = |c: &planner::OrderedCell| (c.sheet, c.row, c.col, c.id, c.owner);
17649                    let mut a: Vec<_> = general.cells.iter().map(key).collect();
17650                    let mut b: Vec<_> = small.iter().map(key).collect();
17651                    a.sort_unstable();
17652                    b.sort_unstable();
17653                    assert_eq!(a, b, "small plan cells differ from the planner");
17654                    assert!(
17655                        general.cells.iter().all(|c| c.cycle.is_none()),
17656                        "small plan of a cyclic request"
17657                    );
17658                }
17659                let adapted = plan_schedule::schedule(
17660                    &small,
17661                    0,
17662                    None,
17663                    |cell| {
17664                        self.graph
17665                            .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
17666                            .ok_or_else(|| failure("missing executor identity".to_owned()))
17667                    },
17668                    |_work| Ok(()),
17669                )
17670                .map_err(|error| match error {
17671                    plan_schedule::ScheduleError::Runtime(error) => error,
17672                    other => failure(format!("{other:?}")),
17673                })?;
17674                if let Some(ledger) = ledger {
17675                    ledger
17676                        .reserve_schedule_discovery(adapted.peak_heap_bytes)
17677                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17678                    ledger
17679                        .release_scratch(adapted.peak_heap_bytes)
17680                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17681                }
17682                return Ok(adapted.schedule);
17683            }
17684        }
17685        // Names are symbol-plane nodes (design §4.1): a name vertex plans as
17686        // the unit at its node, between its precedents and its readers.
17687        // Candidates become cells, sorted by (sheet, column, row) and
17688        // coalesced into row intervals: one cover insert per interval.
17689        let mut cover = Cover::new();
17690        {
17691            let cell_of = |&id: &VertexId| {
17692                self.graph
17693                    .authority_cell_of_vertex(id)
17694                    .map(|(sheet, row, col)| (sheet, col, row))
17695            };
17696            // A full recalc maps and sorts every formula: on the pool when
17697            // there is one (first eval's schedule is serial work otherwise).
17698            let cells: Vec<(u16, u32, u32)> = match self.thread_pool.as_deref() {
17699                Some(pool) if candidates.len() >= PARALLEL_SCHEDULE_MIN_CANDIDATES => {
17700                    use rayon::prelude::*;
17701                    pool.install(|| {
17702                        let mut cells: Vec<_> = candidates.par_iter().filter_map(cell_of).collect();
17703                        cells.par_sort_unstable();
17704                        cells
17705                    })
17706                }
17707                _ => {
17708                    let mut cells: Vec<_> = candidates.iter().filter_map(cell_of).collect();
17709                    cells.sort_unstable();
17710                    cells
17711                }
17712            };
17713            let mut i = 0;
17714            while i < cells.len() {
17715                let (sheet, col, r0) = cells[i];
17716                let mut r1 = r0;
17717                let mut j = i + 1;
17718                while j < cells.len() && cells[j].0 == sheet && cells[j].1 == col {
17719                    if cells[j].2 > r1 + 1 {
17720                        break;
17721                    }
17722                    r1 = r1.max(cells[j].2);
17723                    j += 1;
17724                }
17725                cover.insert_rect(sheet, &Rect::new(r0, col, r1, col));
17726                i = j;
17727            }
17728        }
17729        let mut hints = Vec::new();
17730        for (&reader, deps) in vdeps {
17731            let Some(reader) = self.graph.authority_cell_of_vertex(reader) else {
17732                continue;
17733            };
17734            for &dependency in deps {
17735                let Some(dep) = self.graph.authority_cell_of_vertex(dependency) else {
17736                    continue;
17737                };
17738                hints.push(planner::PlanHint {
17739                    reader: (reader.0, reader.2, reader.1),
17740                    edge: EdgeKey {
17741                        dep_sheet: reader.0,
17742                        tag: Tag::X,
17743                        lk: u32::MAX,
17744                        proj: RefProj {
17745                            sheet: dep.0,
17746                            rows: AxisMap::fixed(dep.1, dep.1),
17747                            cols: AxisMap::fixed(dep.2, dep.2),
17748                        },
17749                    },
17750                });
17751            }
17752        }
17753        // rdi_dyn: order each dynamic reader after its observed reads.
17754        let host = self.graph.authority_host();
17755        for &id in candidates.iter().filter(|_| host.has_observed()) {
17756            let Some(reads) = host.observed(id) else {
17757                continue;
17758            };
17759            let Some(reader) = self.graph.authority_cell_of_vertex(id) else {
17760                continue;
17761            };
17762            for &(sheet, r0, c0, r1, c1) in reads {
17763                hints.push(planner::PlanHint {
17764                    reader: (reader.0, reader.2, reader.1),
17765                    edge: EdgeKey {
17766                        dep_sheet: reader.0,
17767                        tag: Tag::X,
17768                        lk: u32::MAX,
17769                        proj: RefProj {
17770                            sheet,
17771                            rows: AxisMap::fixed(r0, r1),
17772                            cols: AxisMap::fixed(c0, c1),
17773                        },
17774                    },
17775                });
17776            }
17777        }
17778        hints.sort_unstable_by_key(|hint| hint.reader);
17779        let checkpoint = ledger
17780            .as_ref()
17781            .map_or(0, |ledger| ledger.scratch_checkpoint());
17782        let scratch_limit = ledger
17783            .as_ref()
17784            .and_then(|ledger| ledger.schedule_discovery_limit())
17785            .map(|limit| limit.saturating_sub(checkpoint));
17786        let ordered = planner::plan_with_hints(store, &cover, &hints, scratch_limit, None, None)
17787            .map_err(|error| failure(format!("{error:?}")))?;
17788        // max_work_units is an execution budget. Planning work must be capped
17789        // independently: charging it here changes the observable publication
17790        // boundary (e.g. a spill must commit before the next execution fails).
17791        let adapted = plan_schedule::schedule(
17792            &ordered.cells,
17793            ordered.heap_bytes(),
17794            scratch_limit,
17795            |cell| {
17796                self.graph
17797                    .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
17798                    .ok_or_else(|| failure("missing executor identity".to_owned()))
17799            },
17800            |_work| {
17801                self.cancellation_checkpoint("Evaluation cancelled during authority planning")?;
17802                if let Some(ledger) = ledger.as_deref_mut() {
17803                    ledger
17804                        .checkpoint_deadline()
17805                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17806                }
17807                Ok(())
17808            },
17809        )
17810        .map_err(|error| match error {
17811            plan_schedule::ScheduleError::Runtime(error) => error,
17812            other => failure(format!("{other:?}")),
17813        })?;
17814        if let Some(ledger) = ledger {
17815            let peak = ordered.peak_heap_bytes.max(adapted.peak_heap_bytes);
17816            ledger
17817                .reserve_schedule_discovery(peak)
17818                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17819            ledger
17820                .release_scratch(peak)
17821                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
17822        }
17823        // The planner's order is scratch now; a full recalc's is large.
17824        match self.thread_pool.as_deref() {
17825            Some(pool) if ordered.cells.len() >= PARALLEL_SCHEDULE_MIN_CANDIDATES => {
17826                pool.spawn(move || drop(ordered))
17827            }
17828            _ => drop(ordered),
17829        }
17830        Ok(adapted.schedule)
17831    }
17832
17833    /// Static-schedule cache eligibility. Legacy excludes range readers:
17834    /// their order comes from per-request range virtual deps. Under the
17835    /// authority a range read is a static edge of the relation, so only
17836    /// dynamic readers (whose hints are per request) are excluded, and the
17837    /// key adds the authority revision (design §8.4).
17838    fn can_use_static_schedule_cache(&self, to_evaluate: &[VertexId]) -> bool {
17839        {
17840            // A dynamic reader is planned from its observed reads, which the
17841            // key covers (rev.dyn); one without them needs a pre-probe, and
17842            // request-scoped replan hints are never cached.
17843            let host = self.graph.authority_host();
17844            !to_evaluate.is_empty()
17845                && !self.freshness_has_hints()
17846                && to_evaluate
17847                    .iter()
17848                    .all(|&v| !self.graph.is_dynamic(v) || host.observed(v).is_some())
17849        }
17850    }
17851
17852    fn schedule_cache_authority_revision(&self) -> (u64, u64) {
17853        {
17854            // rev.topology is `topology_epoch`; a symbol revision rebuilds the
17855            // store, so it is part of `revision`.
17856            let host = self.graph.authority_host();
17857            (host.revision(), host.rev_dyn())
17858        }
17859    }
17860
17861    fn start_virtual_dep_telemetry(&self) -> VirtualDepTelemetry {
17862        VirtualDepTelemetry {
17863            fallback_mode_activations: self.virtual_dep_fallback_activations,
17864            ..VirtualDepTelemetry::default()
17865        }
17866    }
17867
17868    fn accumulate_schedule_meta(telemetry: &mut VirtualDepTelemetry, meta: &ScheduleBuildMeta) {
17869        telemetry.candidate_vertices_total += meta.candidate_vertices;
17870        telemetry.vdeps_vertices_total += meta.vdeps_vertices;
17871        telemetry.vdeps_edges_total += meta.vdeps_edges;
17872        telemetry.builder_elapsed_ms_total += meta.builder_elapsed_ms;
17873        if meta.schedule_cache_eligible {
17874            if meta.schedule_cache_hit {
17875                telemetry.schedule_cache_hits += 1;
17876                telemetry.reused_schedule_vertices_total += meta.candidate_vertices;
17877            } else {
17878                telemetry.schedule_cache_misses += 1;
17879            }
17880        }
17881        if meta.used_virtual_schedule {
17882            telemetry.schedule_virtual_passes += 1;
17883        } else {
17884            telemetry.schedule_static_passes += 1;
17885        }
17886    }
17887
17888    /// End-of-pass dirty bookkeeping; true when the loop must replan.
17889    /// Legacy: clear the pass, re-dirty readers whose pre-probe changed.
17890    /// Under the authority an armed pass keeps stale and unreached vertices
17891    /// dirty instead (design §8.2, `freshness.rs`).
17892    fn finish_pass_dirty(&mut self, to_evaluate: &[VertexId], changed: &[VertexId]) -> bool {
17893        self.finish_pass_dirty_scoped(to_evaluate, changed, true)
17894    }
17895
17896    /// [`Self::finish_pass_dirty`] for a targeted pass: only its candidates
17897    /// decide whether to replan.
17898    fn finish_target_pass_dirty(&mut self, to_evaluate: &[VertexId], changed: &[VertexId]) -> bool {
17899        self.finish_pass_dirty_scoped(to_evaluate, changed, false)
17900    }
17901
17902    fn finish_pass_dirty_scoped(
17903        &mut self,
17904        to_evaluate: &[VertexId],
17905        changed: &[VertexId],
17906        #[allow(unused_variables)] whole_workbook: bool,
17907    ) -> bool {
17908        self.freshness_finish_pass(to_evaluate, changed, whole_workbook)
17909    }
17910
17911    /// Start a pass over `schedule` (arms the freshness recorder).
17912    fn begin_pass(
17913        &mut self,
17914        #[allow(unused_variables)] schedule: &crate::engine::scheduler::Schedule,
17915    ) {
17916        self.freshness_begin_pass(schedule);
17917    }
17918
17919    /// FR4 layer barrier after unit `index`: true stops the pass.
17920    fn stop_after_unit(
17921        &mut self,
17922        #[allow(unused_variables)] schedule: &crate::engine::scheduler::Schedule,
17923        #[allow(unused_variables)] index: usize,
17924    ) -> bool {
17925        self.freshness_stop_after_unit(schedule, index)
17926    }
17927
17928    fn changed_virtual_dep_vertices(
17929        &mut self,
17930        to_evaluate: &[VertexId],
17931        old_vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
17932    ) -> Vec<VertexId> {
17933        #[cfg(test)]
17934        if self.force_virtual_dep_changes_remaining_for_test > 0
17935            && let Some(vertex) = to_evaluate.first().copied()
17936        {
17937            self.force_virtual_dep_changes_remaining_for_test -= 1;
17938            return vec![vertex];
17939        }
17940        // An armed pass detects stale dynamic reads directly; the pre-probe
17941        // comparison is legacy's substitute for that (design §8.2).
17942        if self.freshness_armed() {
17943            return Vec::new();
17944        }
17945        if !to_evaluate
17946            .iter()
17947            .copied()
17948            .any(|v| self.graph.is_dynamic(v))
17949        {
17950            return Vec::new();
17951        }
17952
17953        let builder = VirtualDepBuilder::new(self);
17954        let (new_vdeps, _) = builder.build(to_evaluate);
17955
17956        let mut candidates = FxHashSet::default();
17957        candidates.extend(old_vdeps.keys().copied());
17958        candidates.extend(new_vdeps.keys().copied());
17959
17960        let mut changed = Vec::new();
17961        for v in candidates {
17962            if old_vdeps.get(&v) != new_vdeps.get(&v) {
17963                changed.push(v);
17964            }
17965        }
17966        changed
17967    }
17968
17969    /// Build a demand-driven subgraph for the given targets, including ephemeral edges for
17970    /// compressed ranges, and returning the set of dirty/volatile precedents and virtual deps.
17971    /// Demand candidates of `targets` and their dynamic plan hints: under
17972    /// `unified_authority` from the authority's relation (design §8.3),
17973    /// otherwise from the legacy graph.
17974    #[allow(clippy::type_complexity)]
17975    fn demand_subgraph(
17976        &self,
17977        targets: &[VertexId],
17978    ) -> Result<
17979        (
17980            Vec<VertexId>,
17981            rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
17982        ),
17983        ExcelError,
17984    > {
17985        self.authority_demand_subgraph(targets)
17986    }
17987
17988    /// Design §8.3: traverse precedents from the targets over the
17989    /// authority's static relation (symbol nodes are ordinary pieces) plus
17990    /// the dynamic readers' virtual dependencies, and collect what legacy's
17991    /// demand walk collects: dirty or volatile formula cells and every name
17992    /// passed through. No legacy dependency structure is read.
17993    #[allow(clippy::type_complexity)]
17994    fn authority_demand_subgraph(
17995        &self,
17996        targets: &[VertexId],
17997    ) -> Result<
17998        (
17999            Vec<VertexId>,
18000            rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
18001        ),
18002        ExcelError,
18003    > {
18004        use crate::engine::authority::geom::{Cell, Rect, SYMBOL_SHEET};
18005        use crate::engine::authority::store::TagFilter;
18006        use rustc_hash::{FxHashMap, FxHashSet};
18007        let store = self
18008            .graph
18009            .authority_plan_store()
18010            .map_err(Self::authority_excel_error)?;
18011        let ids = store.ids();
18012        let mut to_evaluate: FxHashSet<VertexId> = FxHashSet::default();
18013        let mut vdeps: FxHashMap<VertexId, Vec<VertexId>> = FxHashMap::default();
18014        let mut visited: FxHashSet<Cell> = FxHashSet::default();
18015        // (cell, authority id or NO_VID): the id lets the side array
18016        // translate the cell without a hash lookup.
18017        let mut stack: Vec<(Cell, u32)> = Vec::new();
18018        // Formula cells under `rect` on `sheet`: identity runs per column.
18019        let push_formulas = |stack: &mut Vec<(Cell, u32)>,
18020                             visited: &FxHashSet<Cell>,
18021                             sheet: u16,
18022                             rect: Rect| {
18023            for col in rect.c0..=rect.c1 {
18024                ids.visit_runs_in(sheet, col, rect.r0, rect.r1, &mut |h| {
18025                    let run = ids.run(h);
18026                    let r0 = run.row_start.max(rect.r0);
18027                    let r1 = (run.row_start + run.len - 1).min(rect.r1);
18028                    for row in r0..=r1 {
18029                        if !visited.contains(&(sheet, row, col)) {
18030                            stack.push(((sheet, row, col), run.first_id + (row - run.row_start)));
18031                        }
18032                    }
18033                });
18034            }
18035        };
18036        for &v in targets {
18037            if let Some(table) = self.graph.table_by_vertex(v) {
18038                // A table's demand is its range's, as legacy's table vertex
18039                // leads to the cells it covers (its symbol row has no
18040                // precedents).
18041                let (s, e) = (table.range.start, table.range.end);
18042                push_formulas(
18043                    &mut stack,
18044                    &visited,
18045                    s.sheet_id,
18046                    Rect::new(s.coord.row(), s.coord.col(), e.coord.row(), e.coord.col()),
18047                );
18048            } else if let Some(cell) = self.graph.authority_cell_of_vertex(v) {
18049                stack.push((cell, crate::engine::authority::identity::NO_VID));
18050            }
18051        }
18052        let mut hits = Vec::new();
18053        #[cfg(any(test, feature = "benchmark_internal"))]
18054        let (mut probe_vertices, mut probe_clean_formulas, mut probe_edges, mut probe_dynamic) =
18055            (0, 0, 0, 0);
18056        while let Some((cell, id)) = stack.pop() {
18057            if !visited.insert(cell) {
18058                continue;
18059            }
18060            let Some(v) = self.graph.authority_vertex_of_formula(id, cell) else {
18061                continue;
18062            };
18063            if !self.graph.vertex_exists(v) {
18064                continue;
18065            }
18066            #[cfg(any(test, feature = "benchmark_internal"))]
18067            {
18068                probe_vertices += 1;
18069            }
18070            match self.graph.get_vertex_kind(v) {
18071                VertexKind::FormulaScalar | VertexKind::FormulaArray => {
18072                    if self.graph.is_dirty(v) || self.graph.is_volatile(v) {
18073                        to_evaluate.insert(v);
18074                    } else {
18075                        #[cfg(any(test, feature = "benchmark_internal"))]
18076                        {
18077                            probe_clean_formulas += 1;
18078                        }
18079                    }
18080                }
18081                VertexKind::NamedScalar | VertexKind::NamedArray => {
18082                    to_evaluate.insert(v);
18083                }
18084                _ => {}
18085            }
18086            hits.clear();
18087            store.direct_precedents(cell, TagFilter::All, &mut hits);
18088            #[cfg(any(test, feature = "benchmark_internal"))]
18089            {
18090                probe_edges += hits.len();
18091            }
18092            for &(_, sheet, rect) in &hits {
18093                // DirtyExtents: a dirty spill anchor whose extent meets this
18094                // image is a demand precedent, ordered before `v` (§8.2).
18095                if sheet != SYMBOL_SHEET {
18096                    for anchor in self
18097                        .graph
18098                        .spill_anchors_in_region(sheet, rect.r0, rect.c0, rect.r1, rect.c1)
18099                    {
18100                        if anchor != v && self.graph.is_dirty(anchor) {
18101                            vdeps.entry(v).or_default().push(anchor);
18102                            if let Some(c) = self.graph.authority_cell_of_vertex(anchor) {
18103                                stack.push((c, crate::engine::authority::identity::NO_VID));
18104                            }
18105                        }
18106                    }
18107                }
18108                if sheet == SYMBOL_SHEET {
18109                    stack.extend((rect.r0..=rect.r1).map(|slot| {
18110                        (
18111                            (SYMBOL_SHEET, slot, 0),
18112                            crate::engine::authority::identity::NO_VID,
18113                        )
18114                    }));
18115                    continue;
18116                }
18117                push_formulas(&mut stack, &visited, sheet, rect);
18118            }
18119            if self.graph.is_dynamic(v) {
18120                #[cfg(any(test, feature = "benchmark_internal"))]
18121                {
18122                    probe_dynamic += 1;
18123                }
18124                // rdi_dyn: the observed reads are demand precedents.
18125                if let Some(reads) = self.graph.authority_host().observed(v) {
18126                    for &(sheet, r0, c0, r1, c1) in reads {
18127                        push_formulas(&mut stack, &visited, sheet, Rect::new(r0, c0, r1, c1));
18128                    }
18129                }
18130                let (vdeps_map, _) = VirtualDepBuilder::new(self).build(&[v]);
18131                // Pre-probe targets plus reads this request found dirty
18132                // (design §8.3: demand walks rdi ∪ hints).
18133                let hinted = self.freshness_hints(v).unwrap_or(&[]);
18134                if let Some(deps) = vdeps_map
18135                    .get(&v)
18136                    .map(|deps| deps.iter().chain(hinted))
18137                    .or(Some([].iter().chain(hinted)))
18138                {
18139                    for &u in deps {
18140                        vdeps.entry(v).or_default().push(u);
18141                        if let Some(c) = self.graph.authority_cell_of_vertex(u) {
18142                            stack.push((c, crate::engine::authority::identity::NO_VID));
18143                        }
18144                    }
18145                }
18146            }
18147        }
18148        let mut result: Vec<VertexId> = to_evaluate.into_iter().collect();
18149        result.sort_unstable();
18150        for deps in vdeps.values_mut() {
18151            deps.sort_unstable();
18152            deps.dedup();
18153        }
18154        #[cfg(any(test, feature = "benchmark_internal"))]
18155        {
18156            let mut probe = self.recalc_reuse_probe.lock().unwrap();
18157            probe.demand_builds += 1;
18158            probe.demand_vertices += probe_vertices;
18159            probe.demand_clean_formulas += probe_clean_formulas;
18160            probe.demand_explicit_edges += probe_edges;
18161            probe.demand_virtual_builder_calls += probe_dynamic;
18162        }
18163        Ok((result, vdeps))
18164    }
18165
18166    /// Helper: convert 1-based column index to Excel-style letters (1 -> A, 27 -> AA)
18167    fn col_to_letters(col: u32) -> String {
18168        col_letters_from_1based(col).expect("column index must be >= 1")
18169    }
18170
18171    /// Evaluate all dirty/volatile vertices with cancellation support
18172    pub fn evaluate_all_cancellable(
18173        &mut self,
18174        cancel: crate::engine::CancelToken,
18175    ) -> Result<EvalResult, ExcelError> {
18176        self.ensure_not_poisoned()?;
18177        self.observe_evaluation_resource_request(EvaluationRequestKind::FullCancellable, |engine| {
18178            engine.observe_function_semantic_epoch()?;
18179            engine.active_cancel_flag = Some(cancel.clone());
18180            let res = engine.evaluate_all_cancellable_impl(cancel.as_flag());
18181            engine.active_cancel_flag = None;
18182            res
18183        })
18184    }
18185
18186    fn evaluate_all_cancellable_impl(
18187        &mut self,
18188        cancel_flag: &AtomicBool,
18189    ) -> Result<EvalResult, ExcelError> {
18190        let _source_cache = self.source_cache_session();
18191        self.validate_deterministic_mode()?;
18192        if self.config.defer_graph_building {
18193            self.build_graph_all()?;
18194        }
18195        if cancel_flag.load(Ordering::Relaxed) {
18196            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
18197                .with_message("Evaluation cancelled before scheduling".to_string()));
18198        }
18199        self.require_unified_authority()?;
18200        self.begin_evaluation_request();
18201        self.reset_virtual_dep_telemetry_if_disabled();
18202        let start = crate::instant::FzInstant::now();
18203        let mut computed_vertices = 0;
18204        let mut cycle_errors = 0;
18205
18206        let mut replan_iterations = 0;
18207        const MAX_REPLAN: usize = 5;
18208        let mut telemetry = self
18209            .config
18210            .enable_virtual_dep_telemetry
18211            .then(|| self.start_virtual_dep_telemetry());
18212
18213        loop {
18214            if cancel_flag.load(Ordering::Relaxed) {
18215                if let Some(mut t) = telemetry {
18216                    t.bailout_reason = Some("cancelled");
18217                    t.replan_iterations = replan_iterations;
18218                    self.last_virtual_dep_telemetry = t;
18219                }
18220                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
18221                    .with_message("Evaluation cancelled before scheduling".to_string()));
18222            }
18223
18224            let to_evaluate = self.graph.get_evaluation_vertices();
18225            if to_evaluate.is_empty() {
18226                if let Some(t) = telemetry.as_mut()
18227                    && t.bailout_reason.is_none()
18228                {
18229                    t.bailout_reason = Some("no_work");
18230                }
18231                break;
18232            }
18233
18234            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
18235            if let Some(t) = telemetry.as_mut() {
18236                Self::accumulate_schedule_meta(t, &meta);
18237            }
18238
18239            // Walk units in condensation order, checking cancellation between
18240            // units (formerly between cycles and between layers).
18241            self.begin_pass(&schedule);
18242            for (unit_index, &unit) in schedule.units.iter().enumerate() {
18243                match unit {
18244                    ScheduleUnit::Cycle(i) => {
18245                        // Check cancellation between cycles
18246                        if cancel_flag.load(Ordering::Relaxed) {
18247                            if let Some(mut t) = telemetry {
18248                                t.bailout_reason = Some("cancelled");
18249                                t.replan_iterations = replan_iterations;
18250                                self.last_virtual_dep_telemetry = t;
18251                            }
18252                            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
18253                                "Evaluation cancelled during cycle handling".to_string(),
18254                            ));
18255                        }
18256
18257                        if self.handle_cycle_unit(
18258                            schedule.unit_cycle(i),
18259                            None,
18260                            None,
18261                            Some(cancel_flag),
18262                        )? > 0
18263                        {
18264                            cycle_errors += 1;
18265                        }
18266                    }
18267                    ScheduleUnit::Layer(i) => {
18268                        let layer = schedule.unit_layer(i);
18269                        // Check cancellation between layers
18270                        if cancel_flag.load(Ordering::Relaxed) {
18271                            if let Some(mut t) = telemetry {
18272                                t.bailout_reason = Some("cancelled");
18273                                t.replan_iterations = replan_iterations;
18274                                self.last_virtual_dep_telemetry = t;
18275                            }
18276                            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
18277                                .with_message("Evaluation cancelled between layers".to_string()));
18278                        }
18279
18280                        // Evaluate vertices in this layer (parallel or sequential)
18281                        if self.thread_pool.is_some() && layer.vertices.len() > 1 {
18282                            computed_vertices +=
18283                                self.evaluate_layer_parallel_cancellable(layer, cancel_flag)?;
18284                        } else {
18285                            computed_vertices +=
18286                                self.evaluate_layer_sequential_cancellable(layer, cancel_flag)?;
18287                        }
18288                    }
18289                }
18290                if self.stop_after_unit(&schedule, unit_index) {
18291                    break;
18292                }
18293            }
18294
18295            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
18296            if let Some(t) = telemetry.as_mut() {
18297                t.changed_vdeps_total += changed_vertices.len();
18298            }
18299            self.resource_checkpoint(0)?;
18300            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
18301                if let Some(t) = telemetry.as_mut() {
18302                    t.bailout_reason = Some("converged");
18303                }
18304                break;
18305            }
18306            if replan_iterations >= MAX_REPLAN {
18307                if let Some(mut t) = telemetry.take() {
18308                    t.bailout_reason = Some("max_replan");
18309                    t.replan_iterations = replan_iterations;
18310                    self.last_virtual_dep_telemetry = t;
18311                }
18312                return Err(
18313                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
18314                );
18315            }
18316            replan_iterations += 1;
18317        }
18318
18319        if let Some(mut t) = telemetry {
18320            t.replan_iterations = replan_iterations;
18321            self.last_virtual_dep_telemetry = t;
18322        }
18323
18324        // Re-dirty volatile vertices for the next evaluation cycle
18325        self.redirty_for_next_recalc();
18326        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
18327
18328        Ok(EvalResult {
18329            computed_vertices,
18330            cycle_errors,
18331            elapsed: start.elapsed(),
18332        })
18333    }
18334
18335    /// Evaluate only the necessary precedents for specific target cells with cancellation support
18336    pub fn evaluate_until_cancellable(
18337        &mut self,
18338        targets: &[&str],
18339        cancel: crate::engine::CancelToken,
18340    ) -> Result<EvalResult, ExcelError> {
18341        self.ensure_not_poisoned()?;
18342        self.observe_evaluation_resource_request(
18343            EvaluationRequestKind::TargetedCancellable,
18344            |engine| {
18345                engine.observe_function_semantic_epoch()?;
18346                engine.active_cancel_flag = Some(cancel.clone());
18347                let res = engine.evaluate_until_cancellable_impl(targets, cancel.as_flag());
18348                engine.active_cancel_flag = None;
18349                res
18350            },
18351        )
18352    }
18353
18354    fn evaluate_until_cancellable_impl(
18355        &mut self,
18356        targets: &[&str],
18357        cancel_flag: &AtomicBool,
18358    ) -> Result<EvalResult, ExcelError> {
18359        if cancel_flag.load(Ordering::Relaxed) {
18360            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
18361                .with_message("Evaluation cancelled before target preparation"));
18362        }
18363        let mut typed_targets = Vec::with_capacity(targets.len());
18364        for target in targets {
18365            let (sheet, row, col) = self.parse_a1_notation(target)?;
18366            self.graph.sheet_id_mut(&sheet);
18367            typed_targets.push(crate::engine::EvaluationTarget::Cell { sheet, row, col });
18368        }
18369        self.evaluate_mixed_targets(&typed_targets, None)
18370    }
18371
18372    fn parse_a1_notation(&self, address: &str) -> Result<(String, u32, u32), ExcelError> {
18373        let mut quoted = false;
18374        let mut separator = None;
18375        let bytes = address.as_bytes();
18376        let mut index = 0usize;
18377        while index < bytes.len() {
18378            match bytes[index] {
18379                b'\'' => {
18380                    if quoted && bytes.get(index + 1) == Some(&b'\'') {
18381                        index = index.saturating_add(1);
18382                    } else {
18383                        quoted = !quoted;
18384                    }
18385                }
18386                b'!' if !quoted => separator = Some(index),
18387                _ => {}
18388            }
18389            index = index.saturating_add(1);
18390        }
18391        if quoted {
18392            return Err(ExcelError::new(ExcelErrorKind::Ref)
18393                .with_message(format!("Invalid quoted sheet reference `{address}`")));
18394        }
18395        let (sheet, cell_part) = match separator {
18396            Some(separator) => {
18397                let raw_sheet = &address[..separator];
18398                let sheet = if raw_sheet.starts_with('\'') && raw_sheet.ends_with('\'') {
18399                    raw_sheet[1..raw_sheet.len().saturating_sub(1)].replace("''", "'")
18400                } else {
18401                    raw_sheet.to_string()
18402                };
18403                (sheet, &address[separator + 1..])
18404            }
18405            None => (self.default_sheet_name().to_string(), address),
18406        };
18407
18408        let (row, col, _, _) = parse_a1_1based(cell_part).map_err(|err| {
18409            ExcelError::new(ExcelErrorKind::Ref)
18410                .with_message(format!("Invalid cell reference `{cell_part}`: {err}"))
18411        })?;
18412
18413        Ok((sheet, row, col))
18414    }
18415
18416    /// Determine volatility using this engine's FunctionProvider, falling back to global registry.
18417    fn is_ast_volatile_with_provider(&self, ast: &ASTNode) -> bool {
18418        use formualizer_parse::parser::ASTNodeType;
18419        match &ast.node_type {
18420            ASTNodeType::Function { name, args, .. } => {
18421                if let Some(func) = self
18422                    .get_function("", name)
18423                    .or_else(|| crate::function_registry::get("", name))
18424                    && crate::external_callbacks::metadata(
18425                        func.as_ref(),
18426                        crate::function::FnCaps::empty,
18427                        || func.caps(),
18428                    )
18429                    .contains(crate::function::FnCaps::VOLATILE)
18430                {
18431                    return true;
18432                }
18433                args.iter()
18434                    .any(|arg| self.is_ast_volatile_with_provider(arg))
18435            }
18436            ASTNodeType::BinaryOp { left, right, .. } => {
18437                self.is_ast_volatile_with_provider(left)
18438                    || self.is_ast_volatile_with_provider(right)
18439            }
18440            ASTNodeType::UnaryOp { expr, .. } => self.is_ast_volatile_with_provider(expr),
18441            ASTNodeType::Array(rows) => rows.iter().any(|row| {
18442                row.iter()
18443                    .any(|cell| self.is_ast_volatile_with_provider(cell))
18444            }),
18445            _ => false,
18446        }
18447    }
18448
18449    /// Evaluate a layer sequentially
18450    fn evaluate_layer_sequential(
18451        &mut self,
18452        layer: &super::scheduler::Layer,
18453    ) -> Result<usize, ExcelError> {
18454        self.resource_checkpoint(layer.vertices.len() as u64)?;
18455        self.evaluate_layer_sequential_effects(layer)
18456    }
18457
18458    fn update_vertex_value_with_delta(
18459        &mut self,
18460        vertex_id: VertexId,
18461        new_value: LiteralValue,
18462        delta: &mut DeltaCollector,
18463    ) {
18464        if delta.mode != DeltaMode::Off
18465            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
18466        {
18467            let sheet_name = self.graph.sheet_name(cell.sheet_id);
18468            let old = self
18469                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
18470                .unwrap_or(LiteralValue::Empty);
18471            if old != new_value {
18472                delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
18473            }
18474        }
18475        self.graph.update_vertex_value_ref(vertex_id, &new_value);
18476        self.mirror_vertex_value_to_overlay(vertex_id, &new_value);
18477    }
18478
18479    fn evaluate_layer_sequential_with_delta(
18480        &mut self,
18481        layer: &super::scheduler::Layer,
18482        delta: &mut DeltaCollector,
18483    ) -> Result<usize, ExcelError> {
18484        self.resource_checkpoint(layer.vertices.len() as u64)?;
18485        self.evaluate_layer_sequential_with_delta_effects(layer, delta)
18486    }
18487
18488    /// Evaluate a layer sequentially with cancellation support
18489    fn evaluate_layer_sequential_cancellable(
18490        &mut self,
18491        layer: &super::scheduler::Layer,
18492        cancel_flag: &AtomicBool,
18493    ) -> Result<usize, ExcelError> {
18494        self.resource_checkpoint(layer.vertices.len() as u64)?;
18495        self.evaluate_layer_sequential_cancellable_effects(layer, cancel_flag)
18496    }
18497
18498    /// Evaluate a layer sequentially with more frequent cancellation checks for demand-driven evaluation
18499    fn evaluate_layer_sequential_cancellable_demand_driven(
18500        &mut self,
18501        layer: &super::scheduler::Layer,
18502        cancel_flag: &AtomicBool,
18503    ) -> Result<usize, ExcelError> {
18504        self.resource_checkpoint(layer.vertices.len() as u64)?;
18505        self.evaluate_layer_sequential_cancellable_demand_driven_effects(layer, cancel_flag)
18506    }
18507
18508    /// Evaluate a layer in parallel using the thread pool.
18509    ///
18510    /// Cost-adaptive: the layer starts sequentially in slices of doubling
18511    /// size (1, 2, 4, ... vertices, capped so a slice does not overshoot the
18512    /// probe) and hands the rest to the pool once the rest looks worth it
18513    /// (`PARALLEL_LAYER_WORTH` at the rate so far) or the probe
18514    /// (`PARALLEL_LAYER_PROBE`) is spent. A cheap layer never pays the pool's
18515    /// wake-up and join (most Enron layers are tens of µs of work); an
18516    /// expensive one goes parallel after a few vertices. Splitting a layer
18517    /// into consecutive sub-layers is a valid order: its vertices are
18518    /// independent.
18519    fn evaluate_layer_parallel(
18520        &mut self,
18521        layer: &super::scheduler::Layer,
18522    ) -> Result<usize, ExcelError> {
18523        if layer.sequential {
18524            return self.evaluate_layer_sequential(layer);
18525        }
18526        self.resource_checkpoint(layer.vertices.len() as u64)?;
18527        let len = layer.vertices.len();
18528        let buffered = buffer_layer_writes(layer);
18529        let (probe, worth) = (PARALLEL_LAYER_PROBE, PARALLEL_LAYER_WORTH);
18530        let start = crate::instant::FzInstant::now();
18531        let mut pos = 0usize;
18532        let mut step = 1usize;
18533        while pos < len {
18534            if pos > 0 {
18535                let elapsed = start.elapsed();
18536                // Rate so far (ns per vertex) and the rest at that rate.
18537                let per_vertex = elapsed.as_nanos() / pos as u128 + 1;
18538                let rest_estimate = per_vertex * (len - pos) as u128;
18539                if len - pos >= 2 && (elapsed >= probe || rest_estimate >= worth.as_nanos()) {
18540                    let rest = layer.sub_layer(pos, len);
18541                    // Expensive members (a SUMIF over a table) parallelize
18542                    // one per task; cheap ones keep runs of 8 together.
18543                    let min_chunk = if per_vertex >= EXPENSIVE_VERTEX_NS {
18544                        1
18545                    } else {
18546                        8
18547                    };
18548                    return Ok(pos + self.evaluate_layer_parallel_effects(&rest, min_chunk)?);
18549                }
18550                // Next slice: double, but no more than the rest of the probe
18551                // at the rate so far (a slice must not overshoot it).
18552                let fit = (probe.saturating_sub(elapsed).as_nanos() / per_vertex) as usize + 1;
18553                step = step.saturating_mul(2).min(fit);
18554            }
18555            let end = (pos + step).min(len);
18556            let slice = layer.sub_layer(pos, end);
18557            // A slice stops at the probe's end even if its members turn out
18558            // far more expensive than the rate so far predicted.
18559            pos += self.evaluate_layer_units_until(
18560                &slice,
18561                None,
18562                None,
18563                None,
18564                buffered,
18565                Some(start + probe),
18566            )?;
18567        }
18568        Ok(len)
18569    }
18570
18571    fn evaluate_layer_parallel_with_delta(
18572        &mut self,
18573        layer: &super::scheduler::Layer,
18574        delta: &mut DeltaCollector,
18575    ) -> Result<usize, ExcelError> {
18576        if layer.sequential {
18577            return self.evaluate_layer_sequential_with_delta(layer, delta);
18578        }
18579        self.resource_checkpoint(layer.vertices.len() as u64)?;
18580        self.evaluate_layer_parallel_with_delta_effects(layer, delta)
18581    }
18582
18583    /// Evaluate a layer in parallel with cancellation support
18584    fn evaluate_layer_parallel_cancellable(
18585        &mut self,
18586        layer: &super::scheduler::Layer,
18587        cancel_flag: &AtomicBool,
18588    ) -> Result<usize, ExcelError> {
18589        if layer.sequential {
18590            return self.evaluate_layer_sequential_cancellable(layer, cancel_flag);
18591        }
18592        self.resource_checkpoint(layer.vertices.len() as u64)?;
18593        self.evaluate_layer_parallel_cancellable_effects(layer, cancel_flag)
18594    }
18595
18596    /// Evaluate a single vertex without mutating the graph (for parallel evaluation)
18597    fn evaluate_vertex_immutable(&self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
18598        let _callbacks = crate::external_callbacks::enter_if_missing(&self.resolver.callbacks);
18599        // Check if vertex exists
18600        if !self.graph.vertex_exists(vertex_id) {
18601            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
18602                .with_message(format!("Vertex not found: {vertex_id:?}")));
18603        }
18604
18605        // Get vertex kind and check if it needs evaluation
18606        let kind = self.graph.get_vertex_kind(vertex_id);
18607        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
18608
18609        let view = match kind {
18610            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
18611                if let Some(view) = self.graph.formula_view(vertex_id) {
18612                    view
18613                } else {
18614                    return Ok(LiteralValue::Number(0.0));
18615                }
18616            }
18617            VertexKind::Empty | VertexKind::Cell => {
18618                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
18619                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
18620                    let row = cell_ref.coord.row() + 1;
18621                    let col = cell_ref.coord.col() + 1;
18622                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
18623                        return Ok(v);
18624                    }
18625                }
18626                return Ok(LiteralValue::Number(0.0));
18627            }
18628            VertexKind::NamedScalar => {
18629                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
18630                    ExcelError::new(ExcelErrorKind::Name)
18631                        .with_message("Named range metadata missing".to_string())
18632                })?;
18633
18634                return match &named_range.definition {
18635                    NamedDefinition::Cell(cell_ref) => {
18636                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
18637                        Ok(self
18638                            .read_cell_value(
18639                                sheet_name,
18640                                cell_ref.coord.row() + 1,
18641                                cell_ref.coord.col() + 1,
18642                            )
18643                            .unwrap_or(LiteralValue::Empty))
18644                    }
18645                    NamedDefinition::Literal(v) => Ok(v.clone()),
18646                    NamedDefinition::Formula { ast, .. } => {
18647                        let _origin = self
18648                            .operation_diagnostic_collector
18649                            .as_ref()
18650                            .map(|c| c.named_scope(vertex_id));
18651                        let context_sheet = match named_range.scope {
18652                            NameScope::Sheet(id) => id,
18653                            NameScope::Workbook => sheet_id,
18654                        };
18655                        let sheet_name = self.graph.sheet_name(context_sheet);
18656                        let cell_ref = self
18657                            .graph
18658                            .get_cell_ref(vertex_id)
18659                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
18660                        if let Some(result) = self
18661                            .freshness_evaluate_name_recorded(vertex_id, sheet_name, cell_ref, ast)
18662                        {
18663                            return result;
18664                        }
18665                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
18666                            .with_resolved_functions(&self.resolved_functions)
18667                            .with_arena_cells(self);
18668                        interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
18669                    }
18670                    NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
18671                        .with_message("Range-valued name evaluated as scalar".to_string())),
18672                };
18673            }
18674            VertexKind::NamedArray => {
18675                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
18676                    ExcelError::new(ExcelErrorKind::Name)
18677                        .with_message("Named range metadata missing".to_string())
18678                })?;
18679
18680                return match &named_range.definition {
18681                    NamedDefinition::Range(range_ref) => {
18682                        if range_ref.start.sheet_id != range_ref.end.sheet_id {
18683                            return Err(ExcelError::new(ExcelErrorKind::Ref)
18684                                .with_message("Named range cannot span sheets".to_string()));
18685                        }
18686                        let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
18687                        let sr0 = range_ref.start.coord.row();
18688                        let sc0 = range_ref.start.coord.col();
18689                        let er0 = range_ref.end.coord.row();
18690                        let ec0 = range_ref.end.coord.col();
18691                        if sr0 > er0 || sc0 > ec0 {
18692                            return Err(ExcelError::new(ExcelErrorKind::Ref)
18693                                .with_message("Invalid named range bounds".to_string()));
18694                        }
18695
18696                        let h = (er0 - sr0 + 1) as usize;
18697                        let w = (ec0 - sc0 + 1) as usize;
18698                        let cell_count = (h as u64).saturating_mul(w as u64);
18699                        if cell_count > self.config.spill.max_spill_cells as u64 {
18700                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18701                                "Named range too large to materialize as an array".to_string(),
18702                            ));
18703                        }
18704
18705                        // Read numeric serials, resolving the sheet once.
18706                        let sheet_id = range_ref.start.sheet_id;
18707                        let asheet = self.arrow_sheets.sheet(sheet_name);
18708                        let mut rows = Vec::with_capacity(h);
18709                        for r0 in sr0..=er0 {
18710                            let mut row = Vec::with_capacity(w);
18711                            for c0 in sc0..=ec0 {
18712                                row.push(
18713                                    self.read_cell_formatted_in(sheet_id, asheet, r0 + 1, c0 + 1)
18714                                        .0,
18715                                );
18716                            }
18717                            rows.push(row);
18718                        }
18719                        Ok(LiteralValue::Array(rows))
18720                    }
18721                    NamedDefinition::Cell(cell_ref) => {
18722                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
18723                        let row = cell_ref.coord.row() + 1;
18724                        let col = cell_ref.coord.col() + 1;
18725                        let v = self
18726                            .read_cell_value(sheet_name, row, col)
18727                            .unwrap_or(LiteralValue::Empty);
18728                        Ok(LiteralValue::Array(vec![vec![v]]))
18729                    }
18730                    NamedDefinition::Literal(v) => Ok(LiteralValue::Array(vec![vec![v.clone()]])),
18731                    NamedDefinition::Formula { ast, .. } => {
18732                        let _origin = self
18733                            .operation_diagnostic_collector
18734                            .as_ref()
18735                            .map(|c| c.named_scope(vertex_id));
18736                        let context_sheet = match named_range.scope {
18737                            NameScope::Sheet(id) => id,
18738                            NameScope::Workbook => sheet_id,
18739                        };
18740                        let sheet_name = self.graph.sheet_name(context_sheet);
18741                        let cell_ref = self
18742                            .graph
18743                            .get_cell_ref(vertex_id)
18744                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
18745                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
18746                            .with_resolved_functions(&self.resolved_functions)
18747                            .with_arena_cells(self);
18748                        let result = self
18749                            .freshness_evaluate_name_recorded(vertex_id, sheet_name, cell_ref, ast)
18750                            .unwrap_or_else(|| {
18751                                interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
18752                            });
18753                        match result {
18754                            Ok(v) => match v {
18755                                LiteralValue::Array(_) => Ok(v),
18756                                other => Ok(LiteralValue::Array(vec![vec![other]])),
18757                            },
18758                            Err(err) => Ok(LiteralValue::Error(err)),
18759                        }
18760                    }
18761                };
18762            }
18763            VertexKind::InfiniteRange
18764            | VertexKind::Range
18765            | VertexKind::External
18766            | VertexKind::Table => {
18767                // Not directly evaluatable here.
18768                return Ok(LiteralValue::Number(0.0));
18769            }
18770        };
18771
18772        // The interpreter uses a reference to the engine as the context
18773        let sheet_name = self.graph.sheet_name(sheet_id);
18774        let cell_ref = self
18775            .graph
18776            .get_cell_ref(vertex_id)
18777            .expect("cell ref for vertex");
18778        if let Some(result) =
18779            self.freshness_evaluate_recorded(vertex_id, sheet_name, cell_ref, view)
18780        {
18781            return result;
18782        }
18783        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref)
18784            .with_resolved_functions(&self.resolved_functions)
18785            .with_arena_cells(self);
18786
18787        interpreter
18788            .evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
18789            .map(|cv| {
18790                let format = cv.format_id();
18791                self.record_derived_format(vertex_id, format);
18792                self.materialize_formula_result(vertex_id, cv)
18793            })
18794            .or_else(|error| self.fit_formula_error(vertex_id, error))
18795    }
18796
18797    /// Get access to the shared thread pool for parallel evaluation
18798    pub fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
18799        self.thread_pool.as_ref()
18800    }
18801}
18802
18803#[derive(Default)]
18804struct RowBoundsCache {
18805    snapshot: u64,
18806    // key: (sheet_id, col_idx)
18807    map: rustc_hash::FxHashMap<(u32, usize), (Option<u32>, Option<u32>)>,
18808}
18809
18810impl RowBoundsCache {
18811    fn new(snapshot: u64) -> Self {
18812        Self {
18813            snapshot,
18814            map: Default::default(),
18815        }
18816    }
18817    fn get_row_bounds(
18818        &self,
18819        sheet_id: SheetId,
18820        col_idx: usize,
18821        snapshot: u64,
18822    ) -> Option<(Option<u32>, Option<u32>)> {
18823        if self.snapshot != snapshot {
18824            return None;
18825        }
18826        self.map.get(&(sheet_id as u32, col_idx)).copied()
18827    }
18828    fn put_row_bounds(
18829        &mut self,
18830        sheet_id: SheetId,
18831        col_idx: usize,
18832        snapshot: u64,
18833        bounds: (Option<u32>, Option<u32>),
18834    ) {
18835        if self.snapshot != snapshot {
18836            self.snapshot = snapshot;
18837            self.map.clear();
18838        }
18839        self.map.insert((sheet_id as u32, col_idx), bounds);
18840    }
18841}
18842
18843struct UsedAxisBoundsCache {
18844    snapshot: u64,
18845    row_bounds_by_col_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
18846    col_bounds_by_row_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
18847    #[cfg(test)]
18848    row_hits: std::sync::atomic::AtomicUsize,
18849    #[cfg(test)]
18850    row_misses: std::sync::atomic::AtomicUsize,
18851    #[cfg(test)]
18852    col_hits: std::sync::atomic::AtomicUsize,
18853    #[cfg(test)]
18854    col_misses: std::sync::atomic::AtomicUsize,
18855}
18856
18857impl UsedAxisBoundsCache {
18858    fn new(snapshot: u64) -> Self {
18859        Self {
18860            snapshot,
18861            row_bounds_by_col_span: Default::default(),
18862            col_bounds_by_row_span: Default::default(),
18863            #[cfg(test)]
18864            row_hits: std::sync::atomic::AtomicUsize::new(0),
18865            #[cfg(test)]
18866            row_misses: std::sync::atomic::AtomicUsize::new(0),
18867            #[cfg(test)]
18868            col_hits: std::sync::atomic::AtomicUsize::new(0),
18869            #[cfg(test)]
18870            col_misses: std::sync::atomic::AtomicUsize::new(0),
18871        }
18872    }
18873
18874    fn reset_for_snapshot(&mut self, snapshot: u64) {
18875        if self.snapshot != snapshot {
18876            self.snapshot = snapshot;
18877            self.row_bounds_by_col_span.clear();
18878            self.col_bounds_by_row_span.clear();
18879        }
18880    }
18881
18882    fn get_row_bounds(
18883        &self,
18884        sheet_id: SheetId,
18885        start_col: u32,
18886        end_col: u32,
18887        snapshot: u64,
18888    ) -> Option<Option<(u32, u32)>> {
18889        if self.snapshot != snapshot {
18890            return None;
18891        }
18892        let cached = self
18893            .row_bounds_by_col_span
18894            .get(&(sheet_id, start_col, end_col))
18895            .copied();
18896        #[cfg(test)]
18897        if cached.is_some() {
18898            self.row_hits.fetch_add(1, Ordering::Relaxed);
18899        }
18900        cached
18901    }
18902
18903    fn put_row_bounds(
18904        &mut self,
18905        sheet_id: SheetId,
18906        start_col: u32,
18907        end_col: u32,
18908        snapshot: u64,
18909        bounds: Option<(u32, u32)>,
18910    ) {
18911        self.reset_for_snapshot(snapshot);
18912        self.row_bounds_by_col_span
18913            .insert((sheet_id, start_col, end_col), bounds);
18914        #[cfg(test)]
18915        self.row_misses.fetch_add(1, Ordering::Relaxed);
18916    }
18917
18918    fn get_col_bounds(
18919        &self,
18920        sheet_id: SheetId,
18921        start_row: u32,
18922        end_row: u32,
18923        snapshot: u64,
18924    ) -> Option<Option<(u32, u32)>> {
18925        if self.snapshot != snapshot {
18926            return None;
18927        }
18928        let cached = self
18929            .col_bounds_by_row_span
18930            .get(&(sheet_id, start_row, end_row))
18931            .copied();
18932        #[cfg(test)]
18933        if cached.is_some() {
18934            self.col_hits.fetch_add(1, Ordering::Relaxed);
18935        }
18936        cached
18937    }
18938
18939    fn put_col_bounds(
18940        &mut self,
18941        sheet_id: SheetId,
18942        start_row: u32,
18943        end_row: u32,
18944        snapshot: u64,
18945        bounds: Option<(u32, u32)>,
18946    ) {
18947        self.reset_for_snapshot(snapshot);
18948        self.col_bounds_by_row_span
18949            .insert((sheet_id, start_row, end_row), bounds);
18950        #[cfg(test)]
18951        self.col_misses.fetch_add(1, Ordering::Relaxed);
18952    }
18953}
18954
18955// Phase 2 shim: in-process spill manager delegating to current graph methods.
18956#[derive(Default)]
18957pub struct ShimSpillManager {
18958    region_locks: RegionLockManager,
18959    pub(crate) active_locks: rustc_hash::FxHashMap<VertexId, u64>,
18960}
18961
18962impl ShimSpillManager {
18963    pub(crate) fn reserve(
18964        &mut self,
18965        owner: VertexId,
18966        anchor_cell: CellRef,
18967        shape: SpillShape,
18968        _meta: SpillMeta,
18969    ) -> Result<(), ExcelError> {
18970        // Derive region from anchor + shape; enforce in-flight exclusivity only.
18971        let region = crate::engine::spill::Region {
18972            sheet_id: anchor_cell.sheet_id as u32,
18973            row_start: anchor_cell.coord.row(),
18974            row_end: anchor_cell
18975                .coord
18976                .row()
18977                .saturating_add(shape.rows)
18978                .saturating_sub(1),
18979            col_start: anchor_cell.coord.col(),
18980            col_end: anchor_cell
18981                .coord
18982                .col()
18983                .saturating_add(shape.cols)
18984                .saturating_sub(1),
18985        };
18986        match self.region_locks.reserve(region, owner) {
18987            Ok(id) => {
18988                if id != 0 {
18989                    self.active_locks.insert(owner, id);
18990                }
18991                Ok(())
18992            }
18993            Err(e) => Err(e),
18994        }
18995    }
18996
18997    /// Release any in-flight region reservation still held for `owner`.
18998    ///
18999    /// Reservations are normally released on commit/rollback, but if an anchor is
19000    /// abandoned without committing (e.g. cycle detection stamps it with #CIRC), a
19001    /// stale reservation could remain. This is a no-op when nothing is held.
19002    pub(crate) fn release_owner(&mut self, owner: VertexId) {
19003        if let Some(id) = self.active_locks.remove(&owner) {
19004            self.region_locks.release(id);
19005        }
19006    }
19007
19008    pub(crate) fn commit_array_with_value_probe<F>(
19009        &mut self,
19010        graph: &mut DependencyGraph,
19011        anchor_vertex: VertexId,
19012        targets: &[CellRef],
19013        rows: Vec<Vec<LiteralValue>>,
19014        mut value_probe: F,
19015    ) -> Result<(), ExcelError>
19016    where
19017        F: FnMut(&DependencyGraph, &CellRef) -> Option<LiteralValue>,
19018    {
19019        use formualizer_common::{ExcelErrorExtra, ExcelErrorKind};
19020
19021        // Re-run plan on concrete targets before committing to respect blockers.
19022        // This plan checks formula/spill ownership in the graph, but when the graph value cache
19023        // is disabled (Arrow-canonical mode), it cannot see non-empty value blockers.
19024        let plan_res = graph.plan_spill_region(anchor_vertex, targets);
19025        if let Err(e) = plan_res {
19026            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
19027                self.region_locks.release(id);
19028            }
19029            return Err(e);
19030        }
19031
19032        if !graph.value_cache_enabled() {
19033            // Compute expected spill shape from the target rectangle for diagnostics.
19034            let (expected_rows, expected_cols) = if targets.is_empty() {
19035                (0u32, 0u32)
19036            } else {
19037                let mut min_r = u32::MAX;
19038                let mut max_r = 0u32;
19039                let mut min_c = u32::MAX;
19040                let mut max_c = 0u32;
19041                for cell in targets {
19042                    let r = cell.coord.row();
19043                    let c = cell.coord.col();
19044                    min_r = min_r.min(r);
19045                    max_r = max_r.max(r);
19046                    min_c = min_c.min(c);
19047                    max_c = max_c.max(c);
19048                }
19049                (
19050                    max_r.saturating_sub(min_r).saturating_add(1),
19051                    max_c.saturating_sub(min_c).saturating_add(1),
19052                )
19053            };
19054
19055            let anchor_cell = graph
19056                .get_cell_ref(anchor_vertex)
19057                .expect("anchor cell ref for spill commit");
19058
19059            for cell in targets {
19060                // Never treat the anchor as a blocker.
19061                if *cell == anchor_cell {
19062                    continue;
19063                }
19064                // Skip cells already known to be owned by a spill; plan() handled spill conflicts.
19065                if graph.spill_registry_anchor_for_cell(*cell).is_some() {
19066                    continue;
19067                }
19068                // Skip formula vertices in the target region; plan() handled them (or allowed).
19069                if let Some(vid) = graph.get_vertex_id_for_address(cell)
19070                    && vid != anchor_vertex
19071                {
19072                    match graph.get_vertex_kind(vid) {
19073                        crate::engine::vertex::VertexKind::FormulaScalar
19074                        | crate::engine::vertex::VertexKind::FormulaArray => {
19075                            // plan() already approved allowed overwrites.
19076                            continue;
19077                        }
19078                        _ => {}
19079                    }
19080                }
19081
19082                if let Some(v) = value_probe(graph, cell)
19083                    && !matches!(v, LiteralValue::Empty)
19084                {
19085                    if let Some(id) = self.active_locks.remove(&anchor_vertex) {
19086                        self.region_locks.release(id);
19087                    }
19088                    return Err(ExcelError::new(ExcelErrorKind::Spill)
19089                        .with_message("BlockedByValue")
19090                        .with_extra(ExcelErrorExtra::Spill {
19091                            expected_rows,
19092                            expected_cols,
19093                        }));
19094                }
19095            }
19096        }
19097
19098        let commit_res = graph.commit_spill_region_atomic_with_fault(
19099            anchor_vertex,
19100            targets.to_vec(),
19101            rows,
19102            None,
19103        );
19104        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
19105            self.region_locks.release(id);
19106        }
19107        commit_res.map(|_| ())
19108    }
19109
19110    /// Commit a spill and mirror all written cells into Arrow overlay via the owning engine.
19111    pub(crate) fn commit_array_with_overlay<R: EvaluationContext>(
19112        &mut self,
19113        engine: &mut Engine<R>,
19114        anchor_vertex: VertexId,
19115        targets: &[CellRef],
19116        rows: Vec<Vec<LiteralValue>>,
19117    ) -> Result<(), ExcelError> {
19118        if let Err(error) = engine.guard_pending_spill_commit(anchor_vertex, targets) {
19119            self.release_owner(anchor_vertex);
19120            return Err(error);
19121        }
19122        // Re-run plan on concrete targets before committing to respect blockers.
19123        let plan_res = engine.graph.plan_spill_region(anchor_vertex, targets);
19124        if let Err(e) = plan_res {
19125            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
19126                self.region_locks.release(id);
19127            }
19128            return Err(e);
19129        }
19130
19131        let commit_res = engine.graph.commit_spill_region_atomic_with_fault(
19132            anchor_vertex,
19133            targets.to_vec(),
19134            rows.clone(),
19135            None,
19136        );
19137        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
19138            self.region_locks.release(id);
19139        }
19140        commit_res.map(|_| ())?;
19141        engine
19142            .blocked_pending_spills
19143            .retain(|entry| entry.0 != anchor_vertex);
19144        engine.spill_blocker_waiters.forget(anchor_vertex);
19145
19146        // Mirror into Arrow overlay when enabled
19147        if engine.config.arrow_storage_enabled
19148            && engine.config.delta_overlay_enabled
19149            && engine.config.write_formula_overlay_enabled
19150        {
19151            // Expect targets to be a contiguous rectangle row-major starting at some anchor
19152            for (idx, cell) in targets.iter().enumerate() {
19153                let (r_off, c_off) = {
19154                    if rows.is_empty() || rows[0].is_empty() {
19155                        (0usize, 0usize)
19156                    } else {
19157                        let width = rows[0].len();
19158                        (idx / width, idx % width)
19159                    }
19160                };
19161                let v = rows
19162                    .get(r_off)
19163                    .and_then(|r| r.get(c_off))
19164                    .cloned()
19165                    .unwrap_or(LiteralValue::Empty);
19166                let sheet_name = engine.graph.sheet_name(cell.sheet_id).to_string();
19167                engine.mirror_value_to_computed_overlay(
19168                    &sheet_name,
19169                    cell.coord.row() + 1,
19170                    cell.coord.col() + 1,
19171                    &v,
19172                );
19173            }
19174        }
19175        Ok(())
19176    }
19177}
19178
19179impl<R> Engine<R>
19180where
19181    R: EvaluationContext,
19182{
19183    fn resolve_shared_ref(
19184        &self,
19185        reference: &ReferenceType,
19186        current_sheet: &str,
19187    ) -> Result<formualizer_common::SheetRef<'static>, ExcelError> {
19188        use formualizer_common::{
19189            SheetCellRef as SharedCellRef, SheetLocator, SheetRangeRef as SharedRangeRef,
19190            SheetRef as SharedRef,
19191        };
19192
19193        // Preserve anchor flags from the parsed reference when possible.
19194        let sr = match reference {
19195            ReferenceType::Cell {
19196                sheet,
19197                row,
19198                col,
19199                row_abs,
19200                col_abs,
19201            } => {
19202                let row0 = row
19203                    .checked_sub(1)
19204                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19205                let col0 = col
19206                    .checked_sub(1)
19207                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19208                let sheet_loc = match sheet.as_deref() {
19209                    Some(name) => SheetLocator::from_name(name),
19210                    None => SheetLocator::Current,
19211                };
19212                let coord = formualizer_common::RelativeCoord::new(row0, col0, *row_abs, *col_abs);
19213                SharedRef::Cell(SharedCellRef::new(sheet_loc, coord))
19214            }
19215            ReferenceType::Range {
19216                sheet,
19217                start_row,
19218                start_col,
19219                end_row,
19220                end_col,
19221                start_row_abs,
19222                start_col_abs,
19223                end_row_abs,
19224                end_col_abs,
19225            } => {
19226                let sheet_loc = match sheet.as_deref() {
19227                    Some(name) => SheetLocator::from_name(name),
19228                    None => SheetLocator::Current,
19229                };
19230                let sr = start_row
19231                    .map(|r| {
19232                        r.checked_sub(1)
19233                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
19234                    })
19235                    .transpose()?;
19236                let sc = start_col
19237                    .map(|c| {
19238                        c.checked_sub(1)
19239                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
19240                    })
19241                    .transpose()?;
19242                let er = end_row
19243                    .map(|r| {
19244                        r.checked_sub(1)
19245                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
19246                    })
19247                    .transpose()?;
19248                let ec = end_col
19249                    .map(|c| {
19250                        c.checked_sub(1)
19251                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
19252                    })
19253                    .transpose()?;
19254                let range = SharedRangeRef::from_parts(
19255                    sheet_loc,
19256                    sr.map(|idx| formualizer_common::AxisBound::new(idx, *start_row_abs)),
19257                    sc.map(|idx| formualizer_common::AxisBound::new(idx, *start_col_abs)),
19258                    er.map(|idx| formualizer_common::AxisBound::new(idx, *end_row_abs)),
19259                    ec.map(|idx| formualizer_common::AxisBound::new(idx, *end_col_abs)),
19260                )
19261                .map_err(|_| ExcelError::new(ExcelErrorKind::Ref))?;
19262                SharedRef::Range(range)
19263            }
19264            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
19265        };
19266
19267        let current_id = self
19268            .graph
19269            .sheet_id(current_sheet)
19270            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19271
19272        let resolve_loc = |loc: SheetLocator<'_>| -> Result<SheetLocator<'static>, ExcelError> {
19273            match loc {
19274                SheetLocator::Current => Ok(SheetLocator::Id(current_id)),
19275                SheetLocator::Id(id) => Ok(SheetLocator::Id(id)),
19276                SheetLocator::Name(name) => {
19277                    let n = name.as_ref();
19278                    self.graph
19279                        .sheet_id(n)
19280                        .map(SheetLocator::Id)
19281                        .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
19282                }
19283            }
19284        };
19285
19286        match sr {
19287            SharedRef::Cell(cell) => {
19288                let owned = cell.into_owned();
19289                let sheet = resolve_loc(owned.sheet)?;
19290                Ok(SharedRef::Cell(SharedCellRef::new(sheet, owned.coord)))
19291            }
19292            SharedRef::Range(range) => {
19293                let owned = range.into_owned();
19294                let sheet = resolve_loc(owned.sheet)?;
19295                Ok(SharedRef::Range(SharedRangeRef {
19296                    sheet,
19297                    start_row: owned.start_row,
19298                    start_col: owned.start_col,
19299                    end_row: owned.end_row,
19300                    end_col: owned.end_col,
19301                }))
19302            }
19303        }
19304    }
19305}
19306
19307// Implement the resolver traits for the Engine.
19308// This allows the interpreter to resolve references by querying the engine's graph.
19309impl<R> crate::traits::ReferenceResolver for Engine<R>
19310where
19311    R: EvaluationContext,
19312{
19313    fn resolve_cell_reference(
19314        &self,
19315        sheet: Option<&str>,
19316        row: u32,
19317        col: u32,
19318    ) -> Result<LiteralValue, ExcelError> {
19319        // This context-free trait method has no knowledge of the formula's
19320        // current sheet, so an unqualified (`None`) reference cannot be resolved
19321        // here. Previously this fell back to `default_sheet_name()`, which leaked
19322        // the reference onto an unrelated sheet (issue #110). Interpreter paths
19323        // already qualify references with the current sheet before reaching this
19324        // method (see `Interpreter::implicit_intersection_from_reference`), and
19325        // the sheet-aware scalar path goes through `resolve_cell_reference_value`
19326        // with an explicit `current_sheet`. Returning #REF! for an unqualified
19327        // reference here surfaces the missing context instead of silently
19328        // returning data from the wrong sheet.
19329        let Some(sheet_name) = sheet else {
19330            return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
19331                "Unqualified cell reference resolved without sheet context".to_string(),
19332            ));
19333        };
19334        // Prefer engine's unified accessor which consults Arrow store for base values
19335        // and falls back to graph for formulas and stored values.
19336        if let Some(v) = self.read_cell_value(sheet_name, row, col) {
19337            Ok(v)
19338        } else {
19339            // Excel semantics: empty cell coerces to 0 in numeric contexts
19340            Ok(LiteralValue::Number(0.0))
19341        }
19342    }
19343}
19344
19345impl<R> crate::traits::RangeResolver for Engine<R>
19346where
19347    R: EvaluationContext,
19348{
19349    fn resolve_range_reference(
19350        &self,
19351        sheet: Option<&str>,
19352        sr: Option<u32>,
19353        sc: Option<u32>,
19354        er: Option<u32>,
19355        ec: Option<u32>,
19356    ) -> Result<Box<dyn crate::traits::Range>, ExcelError> {
19357        // For now, delegate range resolution to the external resolver.
19358        // A future optimization could be to handle this within the graph.
19359        self.resolver.resolve_range_reference(sheet, sr, sc, er, ec)
19360    }
19361}
19362
19363impl<R> crate::traits::NamedRangeResolver for Engine<R>
19364where
19365    R: EvaluationContext,
19366{
19367    fn resolve_named_range_reference(
19368        &self,
19369        name: &str,
19370    ) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
19371        self.resolver.resolve_named_range_reference(name)
19372    }
19373}
19374
19375impl<R> crate::traits::TableResolver for Engine<R>
19376where
19377    R: EvaluationContext,
19378{
19379    fn resolve_table_reference(
19380        &self,
19381        tref: &formualizer_parse::parser::TableReference,
19382    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
19383        self.resolver.resolve_table_reference(tref)
19384    }
19385}
19386
19387impl<R> crate::traits::SourceResolver for Engine<R>
19388where
19389    R: EvaluationContext,
19390{
19391    fn source_scalar_version(&self, name: &str) -> Option<u64> {
19392        self.resolver.source_scalar_version(name)
19393    }
19394
19395    fn resolve_source_scalar(&self, name: &str) -> Result<LiteralValue, ExcelError> {
19396        self.resolver.resolve_source_scalar(name)
19397    }
19398
19399    fn source_table_version(&self, name: &str) -> Option<u64> {
19400        self.resolver.source_table_version(name)
19401    }
19402
19403    fn resolve_source_table(
19404        &self,
19405        name: &str,
19406    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
19407        self.resolver.resolve_source_table(name)
19408    }
19409}
19410
19411// The Engine is a Resolver because it implements the constituent traits.
19412impl<R> crate::traits::Resolver for Engine<R> where R: EvaluationContext {}
19413
19414// The Engine provides functions by delegating to its internal resolver.
19415impl<R: EvaluationContext> crate::interpreter::ArenaCellResolver for Engine<R> {
19416    fn resolve_arena_range(
19417        &self,
19418        reference: &crate::engine::arena::CompactRefType,
19419        current: SheetId,
19420        row_delta: i64,
19421        col_delta: i64,
19422    ) -> Result<RangeView<'_>, ExcelError> {
19423        use crate::engine::arena::{CompactRefType, SheetKey};
19424        let CompactRefType::Range {
19425            sheet,
19426            start_row,
19427            start_col,
19428            end_row,
19429            end_col,
19430            start_row_abs,
19431            start_col_abs,
19432            end_row_abs,
19433            end_col_abs,
19434        } = reference
19435        else {
19436            return Err(ExcelError::new(ExcelErrorKind::Ref));
19437        };
19438        let sheet_id = match sheet {
19439            None => current,
19440            Some(SheetKey::Id(id)) => *id,
19441            Some(SheetKey::Name(id)) => self
19442                .graph
19443                .sheet_id(self.graph.data_store().resolve_ast_string(*id))
19444                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?,
19445        };
19446        if self.graph.sheet_reg().name(sheet_id).is_empty() {
19447            return Err(ExcelError::new(ExcelErrorKind::Ref));
19448        }
19449        let axis = |value, sentinel, delta, absolute| {
19450            if value == sentinel {
19451                Ok(None)
19452            } else {
19453                crate::interpreter::shift_axis_for_offset(value, delta, absolute).map(Some)
19454            }
19455        };
19456        let sr = axis(*start_row, 0, row_delta, *start_row_abs)?;
19457        let sc = axis(*start_col, 0, col_delta, *start_col_abs)?;
19458        let er = axis(*end_row, u32::MAX, row_delta, *end_row_abs)?;
19459        let ec = axis(*end_col, u32::MAX, col_delta, *end_col_abs)?;
19460        if sr.zip(er).is_some_and(|(s, e)| s > e) || sc.zip(ec).is_some_and(|(s, e)| s > e) {
19461            return Err(ExcelError::new(ExcelErrorKind::Ref));
19462        }
19463        self.resolve_range_coordinates(sheet_id, sr, sc, er, ec)
19464    }
19465
19466    fn resolve_arena_cell(
19467        &self,
19468        sheet: Option<crate::engine::arena::SheetKey>,
19469        current: crate::SheetId,
19470        row: u32,
19471        col: u32,
19472    ) -> Result<(LiteralValue, Option<crate::format::FormatId>), ExcelError> {
19473        use crate::engine::arena::SheetKey;
19474        let (sheet_id, named_arrow) = match sheet {
19475            None => (current, None),
19476            Some(SheetKey::Id(id)) => {
19477                if self.graph.sheet_reg().name(id).is_empty() {
19478                    return Err(ExcelError::new(ExcelErrorKind::Ref));
19479                }
19480                (id, None)
19481            }
19482            Some(SheetKey::Name(id)) => {
19483                let (sheet_id, arrow) = self
19484                    .resolved_sheet_names
19485                    .get(&id)
19486                    .copied()
19487                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19488                (sheet_id, Some(arrow))
19489            }
19490        };
19491        let arrow = named_arrow.unwrap_or_else(|| {
19492            self.resolved_arrow_sheets
19493                .get(usize::from(sheet_id))
19494                .copied()
19495                .flatten()
19496        });
19497        let asheet = arrow
19498            .and_then(|index| self.arrow_sheets.sheets.get(index))
19499            .or_else(|| {
19500                // Graph-only sheets can acquire Arrow storage when an earlier layer
19501                // commits its first formula result. The pass snapshot cannot hold an
19502                // index for that storage yet; retain the original lookup in this case.
19503                let name = match sheet {
19504                    Some(SheetKey::Name(id)) => self.graph.data_store().resolve_ast_string(id),
19505                    _ => self.graph.sheet_name(sheet_id),
19506                };
19507                self.arrow_sheets.sheet(name)
19508            });
19509        Ok(self.read_cell_formatted_in(sheet_id, asheet, row, col))
19510    }
19511}
19512
19513impl<R> crate::traits::FunctionProvider for Engine<R>
19514where
19515    R: EvaluationContext,
19516{
19517    fn planning_semantic_revision(&self) -> Option<u64> {
19518        self.resolver.planning_semantic_revision()
19519    }
19520
19521    fn get_function(
19522        &self,
19523        prefix: &str,
19524        name: &str,
19525    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
19526        self.resolver.get_function(prefix, name)
19527    }
19528
19529    fn get_function_for_planning(
19530        &self,
19531        prefix: &str,
19532        name: &str,
19533    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
19534        self.resolver.get_function_for_planning(prefix, name)
19535    }
19536}
19537
19538impl<R> Engine<R>
19539where
19540    R: EvaluationContext,
19541{
19542    fn resolve_range_coordinates(
19543        &self,
19544        sheet_id: SheetId,
19545        sr: Option<u32>,
19546        sc: Option<u32>,
19547        er: Option<u32>,
19548        ec: Option<u32>,
19549    ) -> Result<RangeView<'_>, ExcelError> {
19550        let sheet_name = self.graph.sheet_name(sheet_id);
19551        let extent = resolve_used_extent_with_fallback(
19552            OpenRangeBounds {
19553                start_row: sr,
19554                start_column: sc,
19555                end_row: er,
19556                end_column: ec,
19557            },
19558            ExtentPolicy::EvaluationCompat {
19559                fallback_row: None,
19560                fallback_column: None,
19561            },
19562            || {
19563                self.sheet_bounds(sheet_name)
19564                    .map(|_| self.config.max_open_ended_rows)
19565            },
19566            || {
19567                self.sheet_bounds(sheet_name)
19568                    .map(|_| self.config.max_open_ended_cols)
19569            },
19570            |first, last| self.used_rows_for_columns(sheet_name, first, last),
19571            |first, last| self.used_cols_for_rows(sheet_name, first, last),
19572        );
19573        let (sr, sc, er, ec) = extent
19574            .map(|e| (e.start_row, e.start_column, e.end_row, e.end_column))
19575            .unwrap_or((1, 1, 0, 0));
19576        if self.force_materialize_range_views {
19577            if er < sr || ec < sc {
19578                return Ok(RangeView::from_owned_rows(
19579                    Vec::new(),
19580                    self.config.date_system,
19581                ));
19582            }
19583            let h = (er - sr + 1) as u64;
19584            let w = (ec - sc + 1) as u64;
19585            if h.saturating_mul(w) <= self.config.spill.max_spill_cells as u64 {
19586                let mut rows = Vec::with_capacity(h as usize);
19587                for r in sr..=er {
19588                    let mut row = Vec::with_capacity(w as usize);
19589                    for c in sc..=ec {
19590                        row.push(
19591                            self.read_cell_value(sheet_name, r, c)
19592                                .unwrap_or(LiteralValue::Empty),
19593                        );
19594                    }
19595                    rows.push(row);
19596                }
19597                return Ok(RangeView::from_owned_rows(rows, self.config.date_system));
19598            }
19599        }
19600        let Some(asheet) = self.arrow_sheet_by_id(sheet_id) else {
19601            return Ok(RangeView::from_owned_rows(
19602                Vec::new(),
19603                self.config.date_system,
19604            ));
19605        };
19606        Ok(if er < sr || ec < sc {
19607            asheet.range_view(1, 1, 0, 0)
19608        } else {
19609            asheet.range_view(
19610                sr.saturating_sub(1) as usize,
19611                sc.saturating_sub(1) as usize,
19612                er.saturating_sub(1) as usize,
19613                ec.saturating_sub(1) as usize,
19614            )
19615        })
19616    }
19617
19618    /// Semantic used coordinates exclude graph-only dependency placeholders.
19619    ///
19620    /// Non-empty base/overlay/computed cells come from Arrow storage, while
19621    /// scalar and array formulas come from graph formula kinds even before
19622    /// their results are materialized. The legacy graph fallback is omitted:
19623    /// `load_packed_to_vertex` entries are either represented by those sources
19624    /// or are `Empty` dependency placeholders, not a third value authority.
19625    pub(crate) fn semantic_used_rows_for_columns(
19626        &self,
19627        sheet: &str,
19628        start_col: u32,
19629        end_col: u32,
19630    ) -> Option<(u32, u32)> {
19631        let arrow_bounds = self
19632            .sheet_store()
19633            .sheet(sheet)
19634            .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
19635        let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
19636        Self::union_used_bounds(arrow_bounds, formula_bounds)
19637    }
19638
19639    pub(crate) fn semantic_used_cols_for_rows(
19640        &self,
19641        sheet: &str,
19642        start_row: u32,
19643        end_row: u32,
19644    ) -> Option<(u32, u32)> {
19645        let arrow_bounds = self
19646            .sheet_store()
19647            .sheet(sheet)
19648            .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
19649        let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
19650        Self::union_used_bounds(arrow_bounds, formula_bounds)
19651    }
19652}
19653
19654// Override EvaluationContext to provide thread pool access
19655impl<R> crate::traits::EvaluationContext for Engine<R>
19656where
19657    R: EvaluationContext,
19658{
19659    fn named_binding_presence(&self, name: &str, current_sheet: &str) -> Option<bool> {
19660        let sheet = self.graph.sheet_id(current_sheet)?;
19661        if self.graph.resolve_name_entry(name, sheet).is_some() {
19662            Some(true)
19663        } else {
19664            self.resolver.named_binding_presence(name, current_sheet)
19665        }
19666    }
19667    fn operation_diagnostic_collector(
19668        &self,
19669    ) -> Option<&crate::operation_diagnostics::OperationDiagnosticCollector> {
19670        self.operation_diagnostic_collector.as_deref()
19671    }
19672
19673    fn clock(&self) -> &dyn crate::timezone::ClockProvider {
19674        &self.clock
19675    }
19676
19677    fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
19678        self.thread_pool.as_ref()
19679    }
19680
19681    fn cancellation_token(&self) -> Option<crate::engine::CancelToken> {
19682        self.active_cancel_flag.clone()
19683    }
19684
19685    fn chunk_hint(&self) -> Option<usize> {
19686        // Use a simple heuristic from configuration (stripe width * height) as a default hint.
19687        let hint =
19688            (self.config.stripe_height as usize).saturating_mul(self.config.stripe_width as usize);
19689        Some(hint.clamp(1024, 1 << 20)) // clamp between 1K and ~1M
19690    }
19691
19692    fn materialized_array_cells(&self) -> u64 {
19693        self.config
19694            .evaluation_budgets
19695            .scratch
19696            .materialized_array_cells
19697            .unwrap_or(crate::engine::resource_ledger::DEFAULT_MATERIALIZED_ARRAY_CELLS)
19698    }
19699
19700    fn volatile_level(&self) -> crate::traits::VolatileLevel {
19701        self.config.volatile_level
19702    }
19703
19704    fn workbook_seed(&self) -> u64 {
19705        self.config.workbook_seed
19706    }
19707
19708    fn recalc_epoch(&self) -> u64 {
19709        self.recalc_epoch
19710    }
19711
19712    fn workbook_sheet_count(&self) -> Option<usize> {
19713        Some(self.graph.sheet_reg().active_len())
19714    }
19715
19716    fn sheet_index_by_name(&self, sheet: &str) -> Option<usize> {
19717        self.graph.sheet_reg().active_position(sheet)
19718    }
19719
19720    fn current_sheet_index(&self, current_sheet: &str) -> Option<usize> {
19721        self.sheet_index_by_name(current_sheet)
19722    }
19723
19724    fn inspect_reference(
19725        &self,
19726        reference: &ReferenceType,
19727        current_sheet: &str,
19728    ) -> Result<Option<ReferenceInfo>, ExcelError> {
19729        let sheet_info = |sheet_name: &str| -> Result<(SheetId, usize), ExcelError> {
19730            let sheet_id = self
19731                .graph
19732                .sheet_id(sheet_name)
19733                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19734            let sheet_index = self
19735                .graph
19736                .sheet_reg()
19737                .active_position_by_id(sheet_id)
19738                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19739            Ok((sheet_id, sheet_index))
19740        };
19741
19742        let cell_info =
19743            |sheet_name: &str, row: u32, col: u32| -> Result<ReferenceInfo, ExcelError> {
19744                let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
19745                let row0 = row
19746                    .checked_sub(1)
19747                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19748                let col0 = col
19749                    .checked_sub(1)
19750                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19751                Ok(ReferenceInfo {
19752                    first_sheet_index: Some(sheet_index),
19753                    sheet_count: Some(1),
19754                    first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
19755                })
19756            };
19757
19758        let range_info = |sheet_name: &str,
19759                          start_row: Option<u32>,
19760                          start_col: Option<u32>|
19761         -> Result<ReferenceInfo, ExcelError> {
19762            let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
19763            let row = start_row.unwrap_or(1);
19764            let col = start_col.unwrap_or(1);
19765            let row0 = row
19766                .checked_sub(1)
19767                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19768            let col0 = col
19769                .checked_sub(1)
19770                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19771            Ok(ReferenceInfo {
19772                first_sheet_index: Some(sheet_index),
19773                sheet_count: Some(1),
19774                first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
19775            })
19776        };
19777
19778        let info = match reference {
19779            ReferenceType::Cell {
19780                sheet, row, col, ..
19781            } => {
19782                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
19783                cell_info(sheet_name, *row, *col)?
19784            }
19785            ReferenceType::Range {
19786                sheet,
19787                start_row,
19788                start_col,
19789                ..
19790            } => {
19791                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
19792                range_info(sheet_name, *start_row, *start_col)?
19793            }
19794            ReferenceType::Cell3D {
19795                sheet_first,
19796                sheet_last,
19797                row,
19798                col,
19799                ..
19800            } => {
19801                let first = cell_info(sheet_first, *row, *col)?;
19802                ReferenceInfo {
19803                    first_sheet_index: first.first_sheet_index,
19804                    sheet_count: self
19805                        .graph
19806                        .sheet_reg()
19807                        .active_span_len(sheet_first, sheet_last),
19808                    first_cell: first.first_cell,
19809                }
19810            }
19811            ReferenceType::Range3D {
19812                sheet_first,
19813                sheet_last,
19814                start_row,
19815                start_col,
19816                ..
19817            } => {
19818                let first = range_info(sheet_first, *start_row, *start_col)?;
19819                ReferenceInfo {
19820                    first_sheet_index: first.first_sheet_index,
19821                    sheet_count: self
19822                        .graph
19823                        .sheet_reg()
19824                        .active_span_len(sheet_first, sheet_last),
19825                    first_cell: first.first_cell,
19826                }
19827            }
19828            ReferenceType::NamedRange(name) => {
19829                let current_id = self
19830                    .graph
19831                    .sheet_id(current_sheet)
19832                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19833                let named = self
19834                    .graph
19835                    .resolve_name_entry(name, current_id)
19836                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19837                match &named.definition {
19838                    NamedDefinition::Cell(cell) => ReferenceInfo {
19839                        first_sheet_index: self
19840                            .graph
19841                            .sheet_reg()
19842                            .active_position_by_id(cell.sheet_id),
19843                        sheet_count: Some(1),
19844                        first_cell: Some(*cell),
19845                    },
19846                    NamedDefinition::Range(range) => ReferenceInfo {
19847                        first_sheet_index: self
19848                            .graph
19849                            .sheet_reg()
19850                            .active_position_by_id(range.start.sheet_id),
19851                        sheet_count: Some(1),
19852                        first_cell: Some(range.start),
19853                    },
19854                    NamedDefinition::Literal(_) | NamedDefinition::Formula { .. } => {
19855                        ReferenceInfo {
19856                            first_sheet_index: None,
19857                            sheet_count: None,
19858                            first_cell: None,
19859                        }
19860                    }
19861                }
19862            }
19863            ReferenceType::Table(tref) => {
19864                let table = self
19865                    .graph
19866                    .resolve_table_entry(&tref.name)
19867                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
19868                ReferenceInfo {
19869                    first_sheet_index: self
19870                        .graph
19871                        .sheet_reg()
19872                        .active_position_by_id(table.range.start.sheet_id),
19873                    sheet_count: Some(1),
19874                    first_cell: Some(table.range.start),
19875                }
19876            }
19877            ReferenceType::External(_) => return Err(ExcelError::new(ExcelErrorKind::Ref)),
19878        };
19879
19880        Ok(Some(info))
19881    }
19882
19883    fn resolve_spill_reference(
19884        &self,
19885        anchor: &ReferenceType,
19886        current_sheet: &str,
19887    ) -> Result<ReferenceType, ExcelError> {
19888        let no_spill = || {
19889            ExcelError::new(ExcelErrorKind::Ref)
19890                .with_message("Spill reference anchor has no current spill")
19891        };
19892        // The rectangle keeps the operand's sheet qualification: an
19893        // unqualified anchor on the current sheet yields an unqualified range.
19894        let mut qualify = true;
19895        let anchor_cell = match anchor {
19896            ReferenceType::Cell {
19897                sheet, row, col, ..
19898            } => {
19899                qualify = sheet.is_some();
19900                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
19901                let sheet_id = self.graph.sheet_id(sheet_name).ok_or_else(no_spill)?;
19902                if *row == 0 || *col == 0 {
19903                    return Err(no_spill());
19904                }
19905                CellRef::new(sheet_id, Coord::from_excel(*row, *col, true, true))
19906            }
19907            ReferenceType::NamedRange(name) => {
19908                let current_id = self.graph.sheet_id(current_sheet).ok_or_else(no_spill)?;
19909                let named = self
19910                    .graph
19911                    .resolve_name_entry(name, current_id)
19912                    .ok_or_else(no_spill)?;
19913                match &named.definition {
19914                    NamedDefinition::Cell(cell) => *cell,
19915                    NamedDefinition::Range(range)
19916                        if range.start.sheet_id == range.end.sheet_id
19917                            && range.start.coord.row() == range.end.coord.row()
19918                            && range.start.coord.col() == range.end.coord.col() =>
19919                    {
19920                        range.start
19921                    }
19922                    _ => {
19923                        return Err(ExcelError::new(ExcelErrorKind::Ref)
19924                            .with_message("Spill reference name must refer to a single cell"));
19925                    }
19926                }
19927            }
19928            _ => {
19929                return Err(ExcelError::new(ExcelErrorKind::Ref)
19930                    .with_message("Spill reference operand must be a single cell"));
19931            }
19932        };
19933        let vertex = self
19934            .graph
19935            .get_vertex_id_for_address(&anchor_cell)
19936            .ok_or_else(no_spill)?;
19937        // Policy: the spill operator applies only to dynamic arrays, never
19938        // to a legacy fixed-extent formula (including a single-cell one).
19939        if self.graph.fixed_single_arrays.contains(&vertex)
19940            || self.graph.fixed_array_shapes.contains_key(&vertex)
19941        {
19942            return Err(ExcelError::new(ExcelErrorKind::Ref)
19943                .with_message("Spill reference to a fixed array formula"));
19944        }
19945        let (first, last) = match self.graph.spill_extent_for_anchor(vertex) {
19946            Some(extent) => extent,
19947            // FORM211: a declared anchor (source spill identity) holding a
19948            // current scalar result is a 1x1 spill. A malformed registry
19949            // entry stays `#REF!`, as do blocked/error/empty results.
19950            None if !self.graph.spill_registry_has_anchor(vertex)
19951                && self.graph.is_current_declared_dynamic_anchor(vertex)
19952                && self.declared_anchor_holds_scalar(anchor_cell) =>
19953            {
19954                (anchor_cell, anchor_cell)
19955            }
19956            None => return Err(no_spill()),
19957        };
19958        let sheet_name = qualify.then(|| self.graph.sheet_name(first.sheet_id).to_string());
19959        let (sr, sc) = (first.coord.row() + 1, first.coord.col() + 1);
19960        let (er, ec) = (last.coord.row() + 1, last.coord.col() + 1);
19961        // The rectangle is concrete, so its bounds are absolute.
19962        Ok(if sr == er && sc == ec {
19963            ReferenceType::cell_with_abs(sheet_name, sr, sc, true, true)
19964        } else {
19965            ReferenceType::Range {
19966                sheet: sheet_name,
19967                start_row: Some(sr),
19968                start_col: Some(sc),
19969                end_row: Some(er),
19970                end_col: Some(ec),
19971                start_row_abs: true,
19972                start_col_abs: true,
19973                end_row_abs: true,
19974                end_col_abs: true,
19975            }
19976        })
19977    }
19978
19979    fn has_formula_at_cell(&self, cell: CellRef) -> Result<bool, ExcelError> {
19980        let sheet_name = self.graph.sheet_name(cell.sheet_id);
19981        if sheet_name.is_empty() {
19982            return Err(ExcelError::new(ExcelErrorKind::Ref));
19983        }
19984        if self.staged_formulas.get(sheet_name).is_some_and(|entries| {
19985            entries
19986                .get(cell.coord.row() + 1, cell.coord.col() + 1)
19987                .is_some()
19988        }) {
19989            return Ok(true);
19990        }
19991        Ok(self
19992            .graph
19993            .get_vertex_for_cell(&cell)
19994            .is_some_and(|vertex| self.graph.is_live_formula_vertex(vertex)))
19995    }
19996
19997    fn formula_text_at_cell(&self, cell: CellRef) -> Result<Option<String>, ExcelError> {
19998        let sheet_name = self.graph.sheet_name(cell.sheet_id);
19999        if sheet_name.is_empty() {
20000            return Err(ExcelError::new(ExcelErrorKind::Ref));
20001        }
20002        let row = cell.coord.row() + 1;
20003        let col = cell.coord.col() + 1;
20004
20005        if let Some(entries) = self.staged_formulas.get(sheet_name)
20006            && let Some(text) = entries.get(row, col)
20007        {
20008            return Ok(Some(if text.starts_with('=') {
20009                text.to_owned()
20010            } else {
20011                format!("={text}")
20012            }));
20013        }
20014
20015        let Some((Some(ast), _)) = self.get_cell(sheet_name, row, col) else {
20016            return Ok(None);
20017        };
20018        Ok(Some(formualizer_parse::pretty::canonical_formula(&ast)))
20019    }
20020
20021    fn used_rows_for_columns(
20022        &self,
20023        sheet: &str,
20024        start_col: u32,
20025        end_col: u32,
20026    ) -> Option<(u32, u32)> {
20027        // Union Arrow-backed used-region with formula rows that have not been materialized yet.
20028        let sheet_id = self.graph.sheet_id(sheet)?;
20029        let snap = self.data_snapshot_id();
20030        if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
20031            guard
20032                .as_ref()
20033                .and_then(|cache| cache.get_row_bounds(sheet_id, start_col, end_col, snap))
20034        }) {
20035            return cached;
20036        }
20037
20038        let arrow_bounds = self
20039            .sheet_store()
20040            .sheet(sheet)
20041            .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
20042        let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
20043        let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
20044            Some(bounds)
20045        } else {
20046            let sc0 = start_col.saturating_sub(1);
20047            let ec0 = end_col.saturating_sub(1);
20048            self.graph
20049                .used_row_bounds_for_columns(sheet_id, sc0, ec0)
20050                .map(|(a0, b0)| (a0 + 1, b0 + 1))
20051        };
20052
20053        if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
20054            guard
20055                .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
20056                .put_row_bounds(sheet_id, start_col, end_col, snap, computed);
20057        }
20058
20059        computed
20060    }
20061
20062    fn used_cols_for_rows(&self, sheet: &str, start_row: u32, end_row: u32) -> Option<(u32, u32)> {
20063        // Union Arrow-backed used-region with formula columns that have not been materialized yet.
20064        let sheet_id = self.graph.sheet_id(sheet)?;
20065        let snap = self.data_snapshot_id();
20066        if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
20067            guard
20068                .as_ref()
20069                .and_then(|cache| cache.get_col_bounds(sheet_id, start_row, end_row, snap))
20070        }) {
20071            return cached;
20072        }
20073
20074        let arrow_bounds = self
20075            .sheet_store()
20076            .sheet(sheet)
20077            .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
20078        let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
20079        let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
20080            Some(bounds)
20081        } else {
20082            let sr0 = start_row.saturating_sub(1);
20083            let er0 = end_row.saturating_sub(1);
20084            self.graph
20085                .used_col_bounds_for_rows(sheet_id, sr0, er0)
20086                .map(|(a0, b0)| (a0 + 1, b0 + 1))
20087        };
20088
20089        if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
20090            guard
20091                .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
20092                .put_col_bounds(sheet_id, start_row, end_row, snap, computed);
20093        }
20094
20095        computed
20096    }
20097
20098    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
20099        let _ = self.graph.sheet_id(sheet)?;
20100        // Excel-like upper bounds; we expose something finite but large.
20101        // Backends may override with real bounds.
20102        Some((1_048_576, 16_384)) // 1048576 rows, 16384 cols (XFD)
20103    }
20104
20105    fn data_snapshot_id(&self) -> u64 {
20106        self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
20107    }
20108
20109    fn backend_caps(&self) -> crate::traits::BackendCaps {
20110        crate::traits::BackendCaps {
20111            streaming: true,
20112            used_region: true,
20113            write: false,
20114            tables: false,
20115            async_stream: false,
20116        }
20117    }
20118
20119    fn build_lookup_index(
20120        &self,
20121        view: &RangeView<'_>,
20122        axis: LookupAxis,
20123    ) -> Option<Arc<LookupIndex>> {
20124        self.build_lookup_index_impl(view, axis)
20125    }
20126
20127    // Flats removed
20128
20129    fn date_system(&self) -> crate::engine::DateSystem {
20130        self.config.date_system
20131    }
20132    /// New: resolve a reference into a RangeView (Phase 2 API)
20133    fn resolve_range_view<'c>(
20134        &'c self,
20135        reference: &ReferenceType,
20136        current_sheet: &str,
20137    ) -> Result<RangeView<'c>, ExcelError> {
20138        match reference {
20139            ReferenceType::External(ext) => {
20140                let name = ext.raw.as_str();
20141                match ext.kind {
20142                    formualizer_parse::parser::ExternalRefKind::Cell { .. } => {
20143                        let Some(source) = self.graph.resolve_source_scalar_entry(name) else {
20144                            return Err(ExcelError::new(ExcelErrorKind::Name)
20145                                .with_message(format!("Undefined name: {name}")));
20146                        };
20147                        let version = source
20148                            .version
20149                            .or_else(|| self.resolver.source_scalar_version(name));
20150                        let v = self.resolve_source_scalar_cached(name, version)?;
20151                        Ok(RangeView::from_owned_rows(
20152                            vec![vec![v]],
20153                            self.config.date_system,
20154                        ))
20155                    }
20156                    formualizer_parse::parser::ExternalRefKind::Range { .. } => {
20157                        let Some(source) = self.graph.resolve_source_table_entry(name) else {
20158                            // A deferred (whole-row/column) external range
20159                            // evaluates to #REF!, as it did before the parser
20160                            // kept it whole (see `unbound_external_range_defers`).
20161                            let kind =
20162                                if crate::engine::refs::unbound_external_range_defers(&ext.kind) {
20163                                    ExcelErrorKind::Ref
20164                                } else {
20165                                    ExcelErrorKind::Name
20166                                };
20167                            return Err(ExcelError::new(kind)
20168                                .with_message(format!("Undefined table: {name}")));
20169                        };
20170                        let version = source
20171                            .version
20172                            .or_else(|| self.resolver.source_table_version(name));
20173                        let key = (name.to_string(), version);
20174                        if let Ok(g) = self.source_cache.read()
20175                            && let Some(view) = g.external_views.get(&key)
20176                        {
20177                            return Ok(view.clone());
20178                        }
20179                        let table = self.resolve_source_table_cached(name, version)?;
20180                        let spec = Some(formualizer_parse::parser::TableSpecifier::Data);
20181                        // Build under the write lock: concurrent readers of one
20182                        // source wait for a single materialization.
20183                        let Ok(mut g) = self.source_cache.write() else {
20184                            return self.source_table_to_range_view(table.as_ref(), &spec);
20185                        };
20186                        if let Some(view) = g.external_views.get(&key) {
20187                            return Ok(view.clone());
20188                        }
20189                        let view = self.source_table_to_range_view(table.as_ref(), &spec)?;
20190                        g.external_views.insert(key, view.clone());
20191                        Ok(view)
20192                    }
20193                }
20194            }
20195            ReferenceType::Range { .. } => {
20196                let shared = self.resolve_shared_ref(reference, current_sheet)?;
20197                let formualizer_common::SheetRef::Range(range) = shared else {
20198                    return Err(ExcelError::new(ExcelErrorKind::Ref));
20199                };
20200                // No context sheet is available here, so an unresolved locator
20201                // is #REF! rather than a guess (issue #110).
20202                let sheet_id = match range.sheet {
20203                    formualizer_common::SheetLocator::Id(id) => id,
20204                    formualizer_common::SheetLocator::Current
20205                    | formualizer_common::SheetLocator::Name(_) => {
20206                        return Err(ExcelError::new(ExcelErrorKind::Ref));
20207                    }
20208                };
20209                let bounded_range = if range.start_row.is_some()
20210                    && range.start_col.is_some()
20211                    && range.end_row.is_some()
20212                    && range.end_col.is_some()
20213                {
20214                    Some(RangeRef::try_from_shared(range.as_ref())?)
20215                } else {
20216                    None
20217                };
20218
20219                let sr = bounded_range
20220                    .as_ref()
20221                    .map(|r| r.start.coord.row() + 1)
20222                    .or_else(|| range.start_row.map(|b| b.index + 1));
20223                let sc = bounded_range
20224                    .as_ref()
20225                    .map(|r| r.start.coord.col() + 1)
20226                    .or_else(|| range.start_col.map(|b| b.index + 1));
20227                let er = bounded_range
20228                    .as_ref()
20229                    .map(|r| r.end.coord.row() + 1)
20230                    .or_else(|| range.end_row.map(|b| b.index + 1));
20231                let ec = bounded_range
20232                    .as_ref()
20233                    .map(|r| r.end.coord.col() + 1)
20234                    .or_else(|| range.end_col.map(|b| b.index + 1));
20235
20236                self.resolve_range_coordinates(sheet_id, sr, sc, er, ec)
20237            }
20238            ReferenceType::Cell { .. } => {
20239                let shared = self.resolve_shared_ref(reference, current_sheet)?;
20240                let formualizer_common::SheetRef::Cell(cell) = shared else {
20241                    return Err(ExcelError::new(ExcelErrorKind::Ref));
20242                };
20243                let addr = CellRef::try_from_shared(cell)?;
20244                let sheet_id = addr.sheet_id;
20245                let sheet_name = self.graph.sheet_name(sheet_id);
20246                let row = addr.coord.row() + 1;
20247                let col = addr.coord.col() + 1;
20248
20249                if self.force_materialize_range_views {
20250                    let v = self
20251                        .read_cell_value(sheet_name, row, col)
20252                        .unwrap_or(LiteralValue::Empty);
20253                    return Ok(RangeView::from_owned_rows(
20254                        vec![vec![v]],
20255                        self.config.date_system,
20256                    ));
20257                }
20258
20259                if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
20260                    let r0 = row.saturating_sub(1) as usize;
20261                    let c0 = col.saturating_sub(1) as usize;
20262                    let rv = asheet.range_view(r0, c0, r0, c0);
20263                    Ok(rv)
20264                } else {
20265                    let v = self
20266                        .read_cell_value(sheet_name, row, col)
20267                        .unwrap_or(LiteralValue::Empty);
20268                    Ok(RangeView::from_owned_rows(
20269                        vec![vec![v]],
20270                        self.config.date_system,
20271                    ))
20272                }
20273            }
20274            ReferenceType::NamedRange(name) => {
20275                if let Some(current_id) = self.graph.sheet_id(current_sheet)
20276                    && let Some(named) = self.graph.resolve_name_entry(name, current_id)
20277                {
20278                    match &named.definition {
20279                        NamedDefinition::Cell(cell_ref) => {
20280                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
20281                            if self.force_materialize_range_views {
20282                                let v = self
20283                                    .read_cell_value(
20284                                        sheet_name,
20285                                        cell_ref.coord.row() + 1,
20286                                        cell_ref.coord.col() + 1,
20287                                    )
20288                                    .unwrap_or(LiteralValue::Empty);
20289                                return Ok(RangeView::from_owned_rows(
20290                                    vec![vec![v]],
20291                                    self.config.date_system,
20292                                ));
20293                            } else {
20294                                let asheet = self
20295                                    .sheet_store()
20296                                    .sheet(sheet_name)
20297                                    .expect("Arrow sheet missing for named cell");
20298                                let r0 = cell_ref.coord.row() as usize;
20299                                let c0 = cell_ref.coord.col() as usize;
20300                                let rv = asheet.range_view(r0, c0, r0, c0);
20301                                return Ok(rv);
20302                            }
20303                        }
20304                        NamedDefinition::Range(range_ref) => {
20305                            let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
20306                            let sr = range_ref.start.coord.row() + 1;
20307                            let sc = range_ref.start.coord.col() + 1;
20308                            let er = range_ref.end.coord.row() + 1;
20309                            let ec = range_ref.end.coord.col() + 1;
20310                            if self.force_materialize_range_views {
20311                                let h = (er.saturating_sub(sr) + 1) as u64;
20312                                let w = (ec.saturating_sub(sc) + 1) as u64;
20313                                let cell_count = h.saturating_mul(w);
20314                                if cell_count <= self.config.spill.max_spill_cells as u64 {
20315                                    let mut rows: Vec<Vec<LiteralValue>> =
20316                                        Vec::with_capacity(h as usize);
20317                                    for r in sr..=er {
20318                                        let mut rowv: Vec<LiteralValue> =
20319                                            Vec::with_capacity(w as usize);
20320                                        for c in sc..=ec {
20321                                            rowv.push(
20322                                                self.read_cell_value(sheet_name, r, c)
20323                                                    .unwrap_or(LiteralValue::Empty),
20324                                            );
20325                                        }
20326                                        rows.push(rowv);
20327                                    }
20328                                    return Ok(RangeView::from_owned_rows(
20329                                        rows,
20330                                        self.config.date_system,
20331                                    ));
20332                                }
20333                            }
20334                            let asheet = self
20335                                .sheet_store()
20336                                .sheet(sheet_name)
20337                                .expect("Arrow sheet missing for named range");
20338                            let sr0 = range_ref.start.coord.row() as usize;
20339                            let sc0 = range_ref.start.coord.col() as usize;
20340                            let er0 = range_ref.end.coord.row() as usize;
20341                            let ec0 = range_ref.end.coord.col() as usize;
20342                            let rv = asheet.range_view(sr0, sc0, er0, ec0);
20343                            return Ok(rv);
20344                        }
20345                        NamedDefinition::Literal(v) => {
20346                            return Ok(RangeView::from_owned_rows(
20347                                vec![vec![v.clone()]],
20348                                self.config.date_system,
20349                            ));
20350                        }
20351                        NamedDefinition::Formula { ast, .. } => {
20352                            // A name that is exactly one external reference
20353                            // reads its source like the reference itself.
20354                            if let ASTNodeType::Reference {
20355                                reference: external @ ReferenceType::External(_),
20356                                ..
20357                            } = &ast.node_type
20358                            {
20359                                return self.resolve_range_view(external, current_sheet);
20360                            }
20361                            if let Some(value) = self.graph.get_value(named.vertex) {
20362                                return Ok(RangeView::from_owned_rows(
20363                                    vec![vec![value]],
20364                                    self.config.date_system,
20365                                ));
20366                            }
20367                        }
20368                    }
20369                }
20370
20371                if let Some(source) = self.graph.resolve_source_scalar_entry(name) {
20372                    let version = source
20373                        .version
20374                        .or_else(|| self.resolver.source_scalar_version(name));
20375                    let v = self.resolve_source_scalar_cached(name, version)?;
20376                    return Ok(RangeView::from_owned_rows(
20377                        vec![vec![v]],
20378                        self.config.date_system,
20379                    ));
20380                }
20381
20382                let data = self.resolver.resolve_named_range_reference(name)?;
20383                Ok(RangeView::from_owned_rows(data, self.config.date_system))
20384            }
20385            ReferenceType::Table(tref) => {
20386                if let Some(table) = self.graph.resolve_table_entry(&tref.name) {
20387                    let sheet_name = self.graph.sheet_name(table.range.start.sheet_id);
20388                    let asheet = self
20389                        .sheet_store()
20390                        .sheet(sheet_name)
20391                        .expect("Arrow sheet missing for table reference");
20392
20393                    let sr0 = table.range.start.coord.row() as usize;
20394                    let sc0 = table.range.start.coord.col() as usize;
20395                    let er0 = table.range.end.coord.row() as usize;
20396                    let ec0 = table.range.end.coord.col() as usize;
20397
20398                    let has_totals = table.totals_row;
20399                    let has_headers = table.header_row;
20400                    let data_sr = if table.empty_body {
20401                        er0.saturating_add(1)
20402                    } else if has_headers {
20403                        sr0.saturating_add(1)
20404                    } else {
20405                        sr0
20406                    };
20407                    let data_er = if has_totals {
20408                        er0.saturating_sub(1)
20409                    } else {
20410                        er0
20411                    };
20412
20413                    let select = |sr: usize, sc: usize, er: usize, ec: usize| {
20414                        if sr > er || sc > ec {
20415                            asheet.range_view(1, 1, 0, 0)
20416                        } else {
20417                            asheet.range_view(sr, sc, er, ec)
20418                        }
20419                    };
20420
20421                    let av = match &tref.specifier {
20422                        None => {
20423                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
20424                                "Table reference without specifier is unsupported".to_string(),
20425                            ));
20426                        }
20427                        Some(formualizer_parse::parser::TableSpecifier::Column(col)) => {
20428                            let Some(idx) = table.col_index(col) else {
20429                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
20430                                    "Column refers to unknown table column".to_string(),
20431                                ));
20432                            };
20433                            let c0 = sc0 + idx;
20434                            select(data_sr, c0, data_er, c0)
20435                        }
20436                        Some(formualizer_parse::parser::TableSpecifier::ColumnRange(
20437                            start,
20438                            end,
20439                        )) => {
20440                            let Some(si) = table.col_index(start) else {
20441                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
20442                                    "Column range refers to unknown column(s)".to_string(),
20443                                ));
20444                            };
20445                            let Some(ei) = table.col_index(end) else {
20446                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
20447                                    "Column range refers to unknown column(s)".to_string(),
20448                                ));
20449                            };
20450                            let (mut a, mut b) = (si, ei);
20451                            if a > b {
20452                                std::mem::swap(&mut a, &mut b);
20453                            }
20454                            let c_start = sc0 + a;
20455                            let c_end = sc0 + b;
20456                            select(data_sr, c_start, data_er, c_end)
20457                        }
20458                        Some(formualizer_parse::parser::TableSpecifier::All)
20459                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
20460                            formualizer_parse::parser::SpecialItem::All,
20461                        )) => select(sr0, sc0, er0, ec0),
20462                        Some(formualizer_parse::parser::TableSpecifier::Data)
20463                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
20464                            formualizer_parse::parser::SpecialItem::Data,
20465                        )) => select(data_sr, sc0, data_er, ec0),
20466                        Some(formualizer_parse::parser::TableSpecifier::Headers)
20467                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
20468                            formualizer_parse::parser::SpecialItem::Headers,
20469                        )) => {
20470                            if !has_headers {
20471                                asheet.range_view(1, 1, 0, 0)
20472                            } else {
20473                                select(sr0, sc0, sr0, ec0)
20474                            }
20475                        }
20476                        Some(formualizer_parse::parser::TableSpecifier::Totals)
20477                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
20478                            formualizer_parse::parser::SpecialItem::Totals,
20479                        )) => {
20480                            if !has_totals {
20481                                asheet.range_view(1, 1, 0, 0)
20482                            } else {
20483                                select(er0, sc0, er0, ec0)
20484                            }
20485                        }
20486                        Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
20487                            formualizer_parse::parser::SpecialItem::ThisRow,
20488                        )) => {
20489                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
20490                                "@ (This Row) requires table-aware context; not yet supported"
20491                                    .to_string(),
20492                            ));
20493                        }
20494                        Some(formualizer_parse::parser::TableSpecifier::Row(_))
20495                        | Some(formualizer_parse::parser::TableSpecifier::Combination(_)) => {
20496                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
20497                                "Complex structured references not yet supported".to_string(),
20498                            ));
20499                        }
20500                    };
20501
20502                    return Ok(av);
20503                }
20504
20505                if let Some(source) = self.graph.resolve_source_table_entry(&tref.name) {
20506                    let version = source
20507                        .version
20508                        .or_else(|| self.resolver.source_table_version(&tref.name));
20509                    let table = self.resolve_source_table_cached(&tref.name, version)?;
20510                    return self.source_table_to_range_view(table.as_ref(), &tref.specifier);
20511                }
20512
20513                // Fallback: materialize via Resolver::resolve_range_like tranche 1.
20514                // A table nobody defines (an unbound reference kept by the
20515                // BestEffort preparation policy) is `#NAME?`, the kind Strict
20516                // reports at preparation, not the resolver's "not implemented".
20517                let boxed = self
20518                    .resolve_range_like(&ReferenceType::Table(tref.clone()))
20519                    .map_err(|e| {
20520                        if e.kind == ExcelErrorKind::NImpl {
20521                            ExcelError::new(ExcelErrorKind::Name)
20522                                .with_message(format!("Unknown table: {}", tref.name))
20523                        } else {
20524                            e
20525                        }
20526                    })?;
20527                let owned = boxed.materialise().into_owned();
20528                Ok(RangeView::from_owned_rows(owned, self.config.date_system))
20529            }
20530            ReferenceType::Cell3D { .. } | ReferenceType::Range3D { .. } => {
20531                Err(ExcelError::new(ExcelErrorKind::NImpl)
20532                    .with_message("3D references are not yet supported".to_string()))
20533            }
20534        }
20535    }
20536
20537    fn resolve_cell_format(
20538        &self,
20539        sheet: Option<&str>,
20540        row: u32,
20541        col: u32,
20542        current_sheet: &str,
20543    ) -> Option<crate::format::FormatId> {
20544        self.effective_format_id(sheet.unwrap_or(current_sheet), row, col)
20545    }
20546
20547    fn format_class(
20548        &self,
20549        format: crate::format::FormatId,
20550    ) -> Option<formualizer_common::numfmt::FormatClass> {
20551        self.format_registry.class(format).cloned()
20552    }
20553
20554    fn record_cell_derived_format(
20555        &self,
20556        sheet: &str,
20557        row: u32,
20558        col: u32,
20559        format: Option<crate::format::FormatId>,
20560    ) {
20561        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
20562            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
20563            self.record_derived_format_at(cell, format);
20564        }
20565    }
20566
20567    fn resolve_cell_reference_value(
20568        &self,
20569        sheet: Option<&str>,
20570        row: u32,
20571        col: u32,
20572        current_sheet: &str,
20573    ) -> Result<LiteralValue, ExcelError> {
20574        let sheet_name = sheet.unwrap_or(current_sheet);
20575        if self.graph.sheet_id(sheet_name).is_none() {
20576            return Err(ExcelError::new(ExcelErrorKind::Ref));
20577        }
20578        Ok(self
20579            .read_cell_value(sheet_name, row, col)
20580            .unwrap_or(LiteralValue::Empty))
20581    }
20582
20583    fn resolve_cell_reference_value_formatted(
20584        &self,
20585        sheet: Option<&str>,
20586        row: u32,
20587        col: u32,
20588        current_sheet: &str,
20589    ) -> Result<(LiteralValue, Option<crate::format::FormatId>), ExcelError> {
20590        // `resolve_cell_reference_value` + `resolve_cell_format` with one
20591        // sheet lookup of each kind.
20592        let sheet_name = sheet.unwrap_or(current_sheet);
20593        let Some(sheet_id) = self.graph.sheet_id(sheet_name) else {
20594            return Err(ExcelError::new(ExcelErrorKind::Ref));
20595        };
20596        let asheet = self.arrow_sheets.sheet(sheet_name);
20597        Ok(self.read_cell_formatted_in(sheet_id, asheet, row, col))
20598    }
20599
20600    fn build_criteria_mask(
20601        &self,
20602        view: &RangeView<'_>,
20603        col_in_view: usize,
20604        pred: &crate::args::CriteriaPredicate,
20605    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
20606        #[cfg(any(test, feature = "test-support"))]
20607        criteria_mask_test_hooks::note_mask(view.dims().0);
20608        if view.dims().1 == 0 {
20609            return None;
20610        }
20611        // If the view is logically open-ended but the backing sheet has no physical rows,
20612        // treat the mask as empty (0-len) rather than attempting to build a huge mask.
20613        let sheet_rows = view.sheet().nrows as usize;
20614        if sheet_rows == 0 || view.start_row() >= sheet_rows {
20615            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
20616        }
20617        self.criteria_mask_cache.get_or_build(
20618            view,
20619            col_in_view,
20620            pred,
20621            (
20622                self.recalc_epoch,
20623                self.topology_epoch,
20624                self.graph.topology_revision(),
20625            ),
20626            || compute_criteria_mask(view, col_in_view, pred),
20627        )
20628    }
20629
20630    fn build_row_visibility_mask(
20631        &self,
20632        view: &RangeView<'_>,
20633        mode: VisibilityMaskMode,
20634    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
20635        self.build_row_visibility_mask_for_view(view, mode)
20636    }
20637
20638    fn nested_subtotal_cells(
20639        &self,
20640        view: &RangeView<'_>,
20641        include_aggregate: bool,
20642    ) -> Option<Vec<(usize, usize, usize)>> {
20643        self.nested_subtotal_cells_for_view(view, include_aggregate)
20644    }
20645}
20646
20647impl<R> Engine<R>
20648where
20649    R: EvaluationContext,
20650{
20651    fn clear_spill_projection_and_mirror(
20652        &mut self,
20653        anchor_vertex: VertexId,
20654        delta: Option<&mut DeltaCollector>,
20655    ) {
20656        let spill_cells = self.graph.spill_cells_to_clear(anchor_vertex);
20657        if spill_cells.is_empty() {
20658            return;
20659        }
20660
20661        if let Some(delta) = delta
20662            && delta.mode != DeltaMode::Off
20663        {
20664            let empty = LiteralValue::Empty;
20665            for cell in spill_cells.iter() {
20666                let sheet_name = self.graph.sheet_name(cell.sheet_id);
20667                let old = self
20668                    .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20669                    .unwrap_or(LiteralValue::Empty);
20670                if old != empty {
20671                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
20672                }
20673            }
20674        }
20675
20676        self.graph.clear_spill_region(anchor_vertex);
20677        self.freshness_note_spill_change(&spill_cells, &[]);
20678        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
20679            self.record_structural_change(scope);
20680        }
20681
20682        if self.config.arrow_storage_enabled
20683            && self.config.delta_overlay_enabled
20684            && self.config.write_formula_overlay_enabled
20685        {
20686            let empty = LiteralValue::Empty;
20687            for cell in spill_cells.iter() {
20688                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
20689                self.mirror_value_to_computed_overlay(
20690                    &sheet_name,
20691                    cell.coord.row() + 1,
20692                    cell.coord.col() + 1,
20693                    &empty,
20694                );
20695            }
20696        }
20697    }
20698
20699    /// Apply the evaluation outcome for one cyclic SCC: stamp `#CIRC!` on its
20700    /// (optionally filtered) members via `stamp_cycle_error`.
20701    ///
20702    /// This is the single per-SCC application point used by every schedule
20703    /// consumer walking `Schedule::units` (pre-work for #112, where cyclic
20704    /// SCCs will gain runtime verdicts instead of an unconditional stamp).
20705    ///
20706    /// `dirty_filter` preserves the recalc-plan quirk: when `Some(dirty)`,
20707    /// only members present in the set are stamped.
20708    ///
20709    /// Returns the number of vertices stamped (0 when a filter excludes every
20710    /// member), so callers can keep their site-specific `cycle_errors`
20711    /// accounting.
20712    fn apply_cycle_outcome(
20713        &mut self,
20714        cycle: &[VertexId],
20715        mut delta: Option<&mut DeltaCollector>,
20716        dirty_filter: Option<&FxHashSet<VertexId>>,
20717    ) -> usize {
20718        let circ_error = LiteralValue::Error(
20719            ExcelError::new(ExcelErrorKind::Circ)
20720                .with_message("Circular dependency detected".to_string()),
20721        );
20722        let mut stamped = 0usize;
20723        for &vertex_id in cycle {
20724            if let Some(filter) = dirty_filter
20725                && !filter.contains(&vertex_id)
20726            {
20727                continue;
20728            }
20729            self.stamp_cycle_error(vertex_id, &circ_error, delta.as_deref_mut());
20730            stamped += 1;
20731        }
20732        stamped
20733    }
20734
20735    /// Stamp a vertex with `#CIRC!` as part of cycle handling.
20736    ///
20737    /// Unlike a bare `update_vertex_value`, this first tears down any spill the
20738    /// vertex previously anchored: it clears the spilled cells, releases the graph
20739    /// spill registry, drops any lingering region reservation, and mirrors the
20740    /// cleared cells into the computed overlay — the same teardown a normal scalar/
20741    /// error result performs (see `apply_non_array_result_from_parallel` /
20742    /// `clear_spill_projection_and_mirror`). Without this, a #CIRC stamp on a former
20743    /// spill anchor would leave stale spilled values and a reserved region behind
20744    /// (issue #111).
20745    ///
20746    /// When `delta` is provided, the cleared spill cells are recorded (by
20747    /// `clear_spill_projection_and_mirror`) and the anchor's own #CIRC change is
20748    /// recorded here, matching how other result paths emit deltas.
20749    fn stamp_cycle_error(
20750        &mut self,
20751        vertex_id: VertexId,
20752        circ_error: &LiteralValue,
20753        mut delta: Option<&mut DeltaCollector>,
20754    ) {
20755        // Tear down any previous spill projection/region before overwriting the anchor.
20756        if self.graph.spill_registry_has_anchor(vertex_id) {
20757            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
20758        }
20759        // Drop any reservation that was never committed (defensive; normally released
20760        // on the prior successful commit).
20761        self.spill_mgr.release_owner(vertex_id);
20762
20763        // Record the anchor's own #CIRC delta, like other result paths.
20764        if let Some(d) = delta
20765            && d.mode != DeltaMode::Off
20766            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
20767        {
20768            let sheet_name = self.graph.sheet_name(cell.sheet_id);
20769            let old = self
20770                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20771                .unwrap_or(LiteralValue::Empty);
20772            if old != *circ_error {
20773                d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
20774            }
20775        }
20776
20777        self.graph.update_vertex_value_ref(vertex_id, circ_error);
20778        self.mirror_vertex_value_to_overlay(vertex_id, circ_error);
20779    }
20780
20781    /// Dispatch point for one `ScheduleUnit::Cycle` (RFC #112, Stage 2).
20782    ///
20783    /// * `CycleDetection::Static` — today's behavior, byte-for-byte: stamp
20784    ///   `#CIRC!` on the (optionally dirty-filtered) members.
20785    /// * `CycleDetection::Runtime` — evaluate the SCC via
20786    ///   [`Self::evaluate_scc_unit`]. The recalc-plan dirty quirk maps to:
20787    ///   no dirty member → skip the task entirely (values stand); any dirty
20788    ///   member → the whole SCC evaluates (an SCC cannot be partially
20789    ///   evaluated).
20790    ///
20791    /// Returns the number of `#CIRC!`-stamped vertices, so call sites can
20792    /// keep their `cycle_errors` accounting (`> 0` ⇒ count the unit).
20793    fn handle_cycle_unit(
20794        &mut self,
20795        cycle: &[VertexId],
20796        mut delta: Option<&mut DeltaCollector>,
20797        dirty_filter: Option<&FxHashSet<VertexId>>,
20798        cancel_flag: Option<&AtomicBool>,
20799    ) -> Result<usize, ExcelError> {
20800        self.resource_checkpoint(cycle.len() as u64)?;
20801        match self.config.cycle.detection {
20802            CycleDetection::Static => {
20803                Ok(self.apply_cycle_outcome(cycle, delta.as_deref_mut(), dirty_filter))
20804            }
20805            CycleDetection::Runtime => {
20806                if let Some(filter) = dirty_filter
20807                    && !cycle.iter().any(|v| filter.contains(v))
20808                {
20809                    return Ok(0);
20810                }
20811                // Both policies share `evaluate_scc_unit`; they differ only
20812                // in the settle loop's live-cycle arm (Error stamps,
20813                // Iterate keeps passing — RFC #113).
20814                self.evaluate_scc_unit(cycle, delta, cancel_flag)
20815            }
20816        }
20817    }
20818
20819    /// Evaluate one statically-cyclic SCC under `CycleDetection::Runtime`
20820    /// (design doc `formualizer-stage2-scc-evaluation-design.md` §3; contract
20821    /// spec §3; Iterate policy arm per RFC #113).
20822    ///
20823    /// Phantom SCCs (live-acyclic) produce ordinary values under both
20824    /// policies; live cycles get `#CIRC!` with live-cycle-only blast radius
20825    /// under `CyclePolicy::Error`, or Excel-style iterative calculation
20826    /// (converge per spec §6 or cap at `max_iterations` passes) under
20827    /// `CyclePolicy::Iterate`. Runs sequentially on the
20828    /// coordinating thread; commits are write-through per member (no
20829    /// `ComputedWriteBuffer` — that buffer is scoped to layer evaluation and
20830    /// always flushed before a Cycle unit runs, G1), so later members' scalar
20831    /// *and* range reads observe earlier members' results through the overlay
20832    /// cascade. Deltas are recorded once per member at end of task (G11).
20833    ///
20834    /// Returns the number of vertices stamped `#CIRC!`.
20835    ///
20836    /// `pub(crate)` so tests can drive SCC shapes (e.g. name-vertex members)
20837    /// that ingest-time cycle rejection makes unreachable via public edits.
20838    pub(crate) fn evaluate_scc_unit(
20839        &mut self,
20840        cycle: &[VertexId],
20841        mut delta: Option<&mut DeltaCollector>,
20842        cancel_flag: Option<&AtomicBool>,
20843    ) -> Result<usize, ExcelError> {
20844        struct SccMember {
20845            vertex: VertexId,
20846            cell: Option<CellRef>,
20847        }
20848
20849        let task_start = crate::instant::FzInstant::now();
20850
20851        // ── 0. Member order (spec §7.13): cells ascending (sheet, row, col);
20852        // name vertices after, lexicographic by folded canonical name; any
20853        // other vertex kind (defensive — `get_evaluation_vertices` only emits
20854        // formula/name kinds) last by id, never evaluated.
20855        let mut cell_members: Vec<(VertexId, CellRef)> = Vec::new();
20856        let mut name_members: Vec<(VertexId, String)> = Vec::new();
20857        let mut other_members: Vec<VertexId> = Vec::new();
20858        for &v in cycle {
20859            match self.graph.get_vertex_kind(v) {
20860                VertexKind::FormulaScalar | VertexKind::FormulaArray => {
20861                    match self.graph.get_cell_ref(v) {
20862                        Some(cell) => cell_members.push((v, cell)),
20863                        None => other_members.push(v),
20864                    }
20865                }
20866                VertexKind::NamedScalar | VertexKind::NamedArray => {
20867                    match self.graph.name_key_for_vertex(v) {
20868                        Some(key) => name_members.push((v, key)),
20869                        None => other_members.push(v),
20870                    }
20871                }
20872                _ => other_members.push(v),
20873            }
20874        }
20875        cell_members.sort_unstable_by_key(|(_, c)| (c.sheet_id, c.coord.row(), c.coord.col()));
20876        name_members.sort_unstable_by(|(av, ak), (bv, bk)| ak.cmp(bk).then(av.cmp(bv)));
20877        other_members.sort_unstable();
20878
20879        let cell_refs: Vec<CellRef> = cell_members.iter().map(|(_, c)| *c).collect();
20880        let name_keys: Vec<String> = name_members.iter().map(|(_, k)| k.clone()).collect();
20881        let mut members: Vec<SccMember> = Vec::with_capacity(cycle.len());
20882        for (v, c) in &cell_members {
20883            members.push(SccMember {
20884                vertex: *v,
20885                cell: Some(*c),
20886            });
20887        }
20888        for (v, _) in &name_members {
20889            members.push(SccMember {
20890                vertex: *v,
20891                cell: None,
20892            });
20893        }
20894        for v in &other_members {
20895            members.push(SccMember {
20896                vertex: *v,
20897                cell: None,
20898            });
20899        }
20900        let n = members.len();
20901        // Indices addressable by the collector (cells + names); `other`
20902        // members can be neither edge sources nor targets.
20903        let recordable = cell_refs.len() + name_keys.len();
20904
20905        let circ_error = LiteralValue::Error(
20906            ExcelError::new(ExcelErrorKind::Circ)
20907                .with_message("Circular dependency detected".to_string()),
20908        );
20909
20910        // ── 0b. Spec-§4 persistence repair: structural edits clear computed
20911        // overlays wholesale (`clear_computed_overlay_after_row/_col`), but
20912        // an iterating member's committed value is cycle STATE, not a
20913        // recomputable cache — and in canonical mode the overlay is its ONLY
20914        // home. If the overlay entry vanished since the last recalc, re-seed
20915        // it from the end-of-recalc snapshot (`iterative_state_values`) so
20916        // pass-1 reads (scalar AND range, via the overlay cascade) observe
20917        // the persisted value instead of silently restarting at Empty→0.
20918        // (Found by the iterate edge corpus: inserting/deleting an unrelated
20919        // row reset accumulators, violating spec §4/§7.15.)
20920        if !self.iterative_state_values.is_empty() {
20921            let restore: Vec<(VertexId, LiteralValue)> = members
20922                .iter()
20923                .filter_map(|m| {
20924                    let cell = m.cell?;
20925                    let persisted = self.iterative_state_values.get(&m.vertex)?;
20926                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
20927                    let overlay = self
20928                        .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20929                        .unwrap_or(LiteralValue::Empty);
20930                    if matches!(overlay, LiteralValue::Empty) {
20931                        Some((m.vertex, persisted.clone()))
20932                    } else {
20933                        None
20934                    }
20935                })
20936                .collect();
20937            for (vertex, value) in restore {
20938                self.mirror_vertex_value_to_overlay(vertex, &value);
20939            }
20940        }
20941
20942        // ── 1. Pre-task value snapshot (overlay-first for cells — G3; the
20943        // graph value map may be evicted in value-cache-disabled mode).
20944        let mut snapshot: Vec<LiteralValue> = members
20945            .iter()
20946            .map(|m| match m.cell {
20947                Some(cell) => {
20948                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
20949                    self.read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20950                        .unwrap_or(LiteralValue::Empty)
20951                }
20952                None => self
20953                    .graph
20954                    .get_value(m.vertex)
20955                    .unwrap_or(LiteralValue::Empty),
20956            })
20957            .collect();
20958
20959        if let Some(state) = &mut self.resumable {
20960            // Seed ownership belongs to this SCC, not to each vertex across
20961            // every cycle it might join after an edit. A new membership uses
20962            // current values, including earlier installed results.
20963            let mut key = cycle.to_vec();
20964            key.sort_unstable();
20965            if cycle.iter().any(|v| state.new_cycle_members.contains(v)) {
20966                // A changed membership supersedes earlier edit-created SCCs
20967                // over these cells. Returning to an older membership later is
20968                // another formation and must not resurrect its retired seed.
20969                state.cycle_seeds.retain(|old, _| {
20970                    state.original_cycles.contains(old)
20971                        || *old == key
20972                        || !cycle.iter().any(|v| old.binary_search(v).is_ok())
20973                });
20974            }
20975            if !state.original_cycles.contains(&key) {
20976                state.new_cycle_members.extend(cycle.iter().copied());
20977            }
20978            let seed_memory_changed = !state.original_cycles.contains(&key);
20979            let seeds = state.cycle_seeds.entry(key).or_default();
20980            for (member, value) in members.iter().zip(&mut snapshot) {
20981                let seed = seeds.entry(member.vertex).or_insert_with(|| value.clone());
20982                *value = seed.clone();
20983            }
20984            if seed_memory_changed {
20985                state.refresh_seed_retained_bytes();
20986            }
20987            for (member, value) in members.iter().zip(&snapshot) {
20988                self.graph.update_vertex_value_ref(member.vertex, value);
20989                self.mirror_vertex_value_to_overlay(member.vertex, value);
20990            }
20991        }
20992
20993        // ── 2. Pre-scan: spill anchors (FormulaArray) are stamped `#CIRC!`
20994        // with full spill teardown (spec §7.9, #115) and excluded from
20995        // evaluation. They stay recordable edge TARGETS (readers see `#CIRC!`
20996        // and propagate). Non-evaluable defensive members are excluded too.
20997        let mut excluded = vec![false; n];
20998        let mut last_value = snapshot.clone();
20999        let mut stamped = 0usize;
21000        for (i, m) in members.iter().enumerate() {
21001            match self.graph.get_vertex_kind(m.vertex) {
21002                VertexKind::FormulaArray => {
21003                    // Deltas for the cleared spill-region cells (non-members)
21004                    // can only be recorded here; the anchor's own delta is
21005                    // covered by the end-of-task snapshot comparison (dedup).
21006                    self.stamp_cycle_error(m.vertex, &circ_error, delta.as_deref_mut());
21007                    excluded[i] = true;
21008                    last_value[i] = circ_error.clone();
21009                    stamped += 1;
21010                }
21011                VertexKind::FormulaScalar | VertexKind::NamedScalar | VertexKind::NamedArray => {}
21012                _ => excluded[i] = true,
21013            }
21014        }
21015
21016        let collector = LiveEdgeCollector::new_with_names(&cell_refs, &name_keys);
21017
21018        // Per-member live out-edges, refreshed whenever a member re-runs.
21019        let mut out_edges: Vec<Vec<u32>> = vec![Vec::new(); n];
21020        // Position of each member in the most recent pass (-1 = did not run).
21021        let mut pos: Vec<i64> = vec![-1; n];
21022        // Whether each member's committed value changed in the most recent pass.
21023        let mut changed = vec![false; n];
21024
21025        // Evaluate-and-commit one member; returns Ok(true) when the member was
21026        // stamped `#CIRC!` (array result — would-be spill anchor, spec §7.9).
21027        macro_rules! run_member {
21028            ($i:expr) => {{
21029                let i: usize = $i;
21030                let m = &members[i];
21031                if i < recordable {
21032                    collector.set_current(i as u32);
21033                }
21034                let value = {
21035                    let ctx = RecordingContext::new(&*self, &collector);
21036                    match self.evaluate_vertex_recorded(m.vertex, &ctx, &collector) {
21037                        Ok(v) => v,
21038                        Err(e) => LiteralValue::Error(e),
21039                    }
21040                };
21041                let is_cell_formula = m.cell.is_some();
21042                if is_cell_formula && matches!(value, LiteralValue::Array(_)) {
21043                    // A member that *would* spill inside an SCC gets the
21044                    // conservative §7.9 verdict. It has never spilled before
21045                    // (a prior spill would make it FormulaArray, pre-stamped
21046                    // above), so there is no projection to tear down.
21047                    self.stamp_cycle_error(m.vertex, &circ_error, None);
21048                    excluded[i] = true;
21049                    stamped += 1;
21050                    changed[i] = last_value[i] != circ_error;
21051                    last_value[i] = circ_error.clone();
21052                } else {
21053                    self.graph.update_vertex_value_ref(m.vertex, &value);
21054                    self.mirror_vertex_value_to_overlay(m.vertex, &value);
21055                    // §7.14 invariant (G2): a formula member must never be
21056                    // shadowed by a user/delta overlay entry, or iteration
21057                    // reads would silently diverge from committed values.
21058                    #[cfg(debug_assertions)]
21059                    if let Some(cell) = m.cell {
21060                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
21061                        debug_assert!(
21062                            self.read_delta_overlay_cell(
21063                                &sheet_name,
21064                                cell.coord.row() + 1,
21065                                cell.coord.col() + 1
21066                            )
21067                            .is_none(),
21068                            "user overlay must never shadow a formula SCC member ({sheet_name}!r{}c{})",
21069                            cell.coord.row() + 1,
21070                            cell.coord.col() + 1
21071                        );
21072                    }
21073                    changed[i] = last_value[i] != value;
21074                    last_value[i] = value;
21075                }
21076            }};
21077        }
21078
21079        let check_cancel = |flag: Option<&AtomicBool>| -> Result<(), ExcelError> {
21080            if let Some(flag) = flag
21081                && flag.load(Ordering::Relaxed)
21082            {
21083                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
21084                    .with_message("Evaluation cancelled during SCC evaluation".to_string()));
21085            }
21086            Ok(())
21087        };
21088
21089        // ── 3. Pass 1: all evaluable members in member order.
21090        check_cancel(cancel_flag)?;
21091        let mut passes = 1usize;
21092        {
21093            let mut p = 0i64;
21094            for i in 0..n {
21095                if excluded[i] {
21096                    continue;
21097                }
21098                run_member!(i);
21099                pos[i] = p;
21100                p += 1;
21101            }
21102        }
21103
21104        // ── 4. Settle loop (design doc §3 step 4; RFC #113 policy arm).
21105        //
21106        // Acyclic classifications settle stale readers exactly (identical
21107        // under both policies — phantom SCCs never iterate). A witnessed
21108        // live cycle dispatches on policy: `Error` stamps `#CIRC!` and
21109        // stops; `Iterate` keeps running full passes over all members in
21110        // member order until converged (spec §6) or capped at
21111        // `max_iterations` total passes. A live cycle that only appears
21112        // mid-settle takes the same arm, and a cycle that dissolves
21113        // mid-iteration falls back to exact acyclic settling.
21114        //
21115        // Defensive acyclic budget: the acyclic settle is monotone, so more
21116        // than |SCC| + 2 settle passes can only be a bug; cap hits stamp the
21117        // remainder and set telemetry. Tracked via `settle_passes` so
21118        // iteration passes (legitimately many) don't consume the budget.
21119        let policy = self.config.cycle.policy;
21120        let cap = n + 2;
21121        let mut witnessed_cycles = 0usize;
21122        let mut capped = false;
21123        // ── Iterate-policy state ──
21124        let mut iterating = false;
21125        let mut converged = false;
21126        let mut exact_fixed_point = false;
21127        // Values committed by the last *full* pass; `None` until the first
21128        // iteration pass runs (pass 1 has no predecessor to compare against)
21129        // and reset when a settle pass runs (no cross-kind comparisons).
21130        let mut prev_pass: Option<Vec<LiteralValue>> = None;
21131        // Final-round convergence stats (overwritten per round so the values
21132        // reported are the ones observed at stop).
21133        let mut iter_max_delta = 0f64;
21134        let mut iter_nan_converged = 0usize;
21135        // Acyclic stale-reader re-eval passes (defensive budget; under pure
21136        // Error flow `1 + settle_passes == passes`, preserving Stage-2
21137        // behavior exactly).
21138        let mut settle_passes = 0usize;
21139        loop {
21140            // Drain this pass's recordings; members that ran replace their
21141            // out-edge set, members that didn't keep last-known edges.
21142            let drained = collector.take_edges();
21143            for i in 0..n {
21144                if pos[i] >= 0 {
21145                    out_edges[i].clear();
21146                }
21147            }
21148            for (from, to) in drained {
21149                debug_assert!(
21150                    pos[from as usize] >= 0,
21151                    "edge from a member that did not run"
21152                );
21153                out_edges[from as usize].push(to);
21154            }
21155            let mut edges: Vec<(u32, u32)> = Vec::new();
21156            for (i, outs) in out_edges.iter().enumerate() {
21157                if excluded[i] {
21158                    continue;
21159                }
21160                for &t in outs {
21161                    edges.push((i as u32, t));
21162                }
21163            }
21164            edges.sort_unstable();
21165            edges.dedup();
21166
21167            let analysis = analyze_live_graph(n, &edges);
21168
21169            if analysis.cycle_count > 0 {
21170                // Classification repeats every iteration pass under
21171                // `Iterate`; record the widest single witness instead of
21172                // accumulating so the count stays "distinct live cycles".
21173                witnessed_cycles = witnessed_cycles.max(analysis.cycle_count);
21174                match policy {
21175                    CyclePolicy::Error => {
21176                        // POLICY (Error): stamp every member of a live cycle,
21177                        // then one settling pass over the remaining members in
21178                        // live-topological order so error propagation
21179                        // downstream is consistent (spec §3.4). Blast radius =
21180                        // live cycles only.
21181                        for i in 0..n {
21182                            if analysis.in_cycle[i] && !excluded[i] {
21183                                self.stamp_cycle_error(members[i].vertex, &circ_error, None);
21184                                excluded[i] = true;
21185                                last_value[i] = circ_error.clone();
21186                                stamped += 1;
21187                            }
21188                        }
21189                        check_cancel(cancel_flag)?;
21190                        let order: Vec<usize> = analysis
21191                            .topo
21192                            .iter()
21193                            .map(|&i| i as usize)
21194                            .filter(|&i| !excluded[i])
21195                            .collect();
21196                        if !order.is_empty() {
21197                            passes += 1;
21198                            for i in order {
21199                                run_member!(i);
21200                            }
21201                        }
21202                        break;
21203                    }
21204                    CyclePolicy::Iterate {
21205                        max_iterations,
21206                        max_change,
21207                    } => {
21208                        // POLICY (Iterate), spec §3.5/§6.
21209                        iterating = true;
21210
21211                        // Convergence test: the full pass that just completed
21212                        // vs the previous full pass, per the spec-§6 rules.
21213                        // `prev_pass` is `None` until an iteration pass has
21214                        // run — pass 1 has no predecessor, so no convergence
21215                        // test occurs before the second pass (spec §6).
21216                        if let Some(prev) = &prev_pass {
21217                            let mut round_max_delta = 0f64;
21218                            let mut round_nan = 0usize;
21219                            let mut all_converged = true;
21220                            let mut round_exact = true;
21221                            for i in 0..n {
21222                                if excluded[i] {
21223                                    // Stamped mid-iteration (array result,
21224                                    // §7.9): the value is pinned and cannot
21225                                    // change again — trivially settled.
21226                                    continue;
21227                                }
21228                                let out = crate::engine::convergence::values_converged(
21229                                    &prev[i],
21230                                    &last_value[i],
21231                                    max_change,
21232                                    self.config.date_system,
21233                                );
21234                                if out.nan_converged {
21235                                    round_nan += 1;
21236                                }
21237                                if let Some(d) = out.abs_delta {
21238                                    round_max_delta = round_max_delta.max(d);
21239                                    if d != 0.0 {
21240                                        round_exact = false;
21241                                    }
21242                                }
21243                                if !out.converged {
21244                                    all_converged = false;
21245                                }
21246                            }
21247                            // Overwrite (not max): telemetry reports the
21248                            // round observed at stop.
21249                            iter_max_delta = round_max_delta;
21250                            iter_nan_converged = round_nan;
21251                            if all_converged {
21252                                exact_fixed_point = round_exact;
21253                                converged = true;
21254                                break;
21255                            }
21256                        }
21257
21258                        // ── Pass-counting reconciliation (spec §6/§7.6):
21259                        // `max_iterations` counts TOTAL passes, pass 1
21260                        // included, and pass 1 has already run by the time a
21261                        // live cycle is first witnessed here. The budget is
21262                        // therefore checked BEFORE evaluating anything more:
21263                        // with `max_iterations: 1` we stop right here — each
21264                        // member was evaluated exactly once this recalc (the
21265                        // Excel accumulator contract) and no convergence test
21266                        // ran (`prev_pass` is still `None`). Capping keeps
21267                        // the last committed values and is NOT an error
21268                        // (Excel parity); telemetry records it.
21269                        if passes >= max_iterations as usize {
21270                            capped = true;
21271                            break;
21272                        }
21273
21274                        check_cancel(cancel_flag)?;
21275                        // One more full pass over every evaluable member in
21276                        // member order (Gauss–Seidel: each commit is visible
21277                        // to later members within the pass). Live edges
21278                        // re-record — guards can flip near convergence
21279                        // (§7.3) — so classification repeats next time
21280                        // around, and a cycle that dissolves drops back to
21281                        // the exact acyclic settle below.
21282                        prev_pass = Some(last_value.clone());
21283                        for x in pos.iter_mut() {
21284                            *x = -1;
21285                        }
21286                        changed.fill(false);
21287                        passes += 1;
21288                        let mut p = 0i64;
21289                        for i in 0..n {
21290                            if excluded[i] {
21291                                continue;
21292                            }
21293                            run_member!(i);
21294                            pos[i] = p;
21295                            p += 1;
21296                        }
21297                        continue;
21298                    }
21299                }
21300            }
21301
21302            // Acyclic: find stale readers — members whose live read of `to`
21303            // happened before `to`'s value changed in the pass that just ran.
21304            let mut stale: Vec<usize> = Vec::new();
21305            for i in 0..n {
21306                if excluded[i] {
21307                    continue;
21308                }
21309                let is_stale = out_edges[i].iter().any(|&t| {
21310                    let t = t as usize;
21311                    changed[t] && (pos[i] < 0 || (pos[t] >= 0 && pos[i] < pos[t]))
21312                });
21313                if is_stale {
21314                    stale.push(i);
21315                }
21316            }
21317            if stale.is_empty() {
21318                break; // values exact — phantom SCC (or dissolved live cycle)
21319            }
21320            if 1 + settle_passes >= cap {
21321                // Defensive only; hitting this is a bug (loud telemetry).
21322                capped = true;
21323                for (i, m) in members.iter().enumerate() {
21324                    if !excluded[i] {
21325                        self.stamp_cycle_error(m.vertex, &circ_error, None);
21326                        excluded[i] = true;
21327                        last_value[i] = circ_error.clone();
21328                        stamped += 1;
21329                    }
21330                }
21331                break;
21332            }
21333
21334            check_cancel(cancel_flag)?;
21335            // Re-evaluate stale readers in live-topo order, recording fresh
21336            // edges (branches may flip on re-eval — spec §7.3 — which is why
21337            // classification repeats).
21338            // A settle pass is a partial sweep: drop the full-pass baseline
21339            // so a live cycle (re)appearing afterwards never compares values
21340            // across mixed pass kinds.
21341            prev_pass = None;
21342            let topo_pos = analysis.topo_positions();
21343            stale.sort_unstable_by_key(|&i| topo_pos[i]);
21344            for x in pos.iter_mut() {
21345                *x = -1;
21346            }
21347            changed.fill(false);
21348            passes += 1;
21349            settle_passes += 1;
21350            for (p, i) in stale.into_iter().enumerate() {
21351                run_member!(i);
21352                pos[i] = p as i64;
21353            }
21354        }
21355
21356        // Post-work boundary: members are committed write-through, so a
21357        // member that ran across a live cancellation is already visible; the
21358        // task still must not complete (no delta, retention or iteration
21359        // state). The members stay dirty for the retry, as with the
21360        // mid-task checks above.
21361        self.live_cancellation_after_work(SCC_CANCELLED)?;
21362
21363        // Iteration that ended because the live cycle dissolved and the
21364        // acyclic settle reached exactness counts as converged (values are
21365        // exact, strictly better than threshold-converged). The defensive
21366        // settle cap (`capped` + stamping) is not.
21367        if iterating && !converged && !capped {
21368            converged = true;
21369        }
21370
21371        // ── 5. End of task: one delta per member whose final value differs
21372        // from the pre-task snapshot (spec §3 side-effect rule, G11).
21373        collector.clear_current();
21374        if let Some(d) = delta
21375            && d.mode != DeltaMode::Off
21376        {
21377            for (i, m) in members.iter().enumerate() {
21378                if let Some(cell) = m.cell
21379                    && last_value[i] != snapshot[i]
21380                {
21381                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
21382                }
21383            }
21384        }
21385
21386        // Members of an SCC that iterated re-evaluate on EVERY recalc, like
21387        // Excel's circular cells: register them for the end-of-recalc
21388        // volatile-like redirty (see `pending_iterative_redirty`). Marking
21389        // any one member propagates around the (strongly connected) SCC and
21390        // to downstream dependents, but all members are registered so the
21391        // contract survives partial structural edits between recalcs.
21392        //
21393        // Exception (#368): an SCC that stopped on an
21394        // exact fixed point — every member reproduced its previous value
21395        // bit-for-bit before the pass cap, no NaN identity, no volatile or
21396        // dynamic-reference member — cannot change on a re-run with the same
21397        // inputs, so it is retained clean. The dirty graph decides when it
21398        // runs again. Tolerance-only convergence (|Δ| < max_change but ≠ 0),
21399        // capped SCCs (including the `max_iterations: 1` accumulator
21400        // contract) and volatile cycles keep the per-recalc redirty.
21401        // Whatever the outcome, this task supersedes any earlier retention
21402        // of its members.
21403        if !self.retained_scc_members.is_empty() {
21404            for m in members.iter() {
21405                self.retained_scc_members.remove(&m.vertex);
21406            }
21407        }
21408        if iterating {
21409            let retain = converged
21410                && !capped
21411                && exact_fixed_point
21412                && iter_nan_converged == 0
21413                && members
21414                    .iter()
21415                    .all(|m| !self.graph.is_volatile(m.vertex) && !self.graph.is_dynamic(m.vertex));
21416            if retain {
21417                if self.retained_scc_members.is_empty() {
21418                    self.retained_scc_config_fingerprint = self.retained_scc_config_fingerprint();
21419                    self.retained_scc_function_epoch_seen =
21420                        crate::function_registry::semantic_epoch();
21421                    self.retained_scc_provider_revision_seen =
21422                        self.resolver.planning_semantic_revision();
21423                }
21424                let scc_id = self.next_retained_scc_id;
21425                self.next_retained_scc_id = self.next_retained_scc_id.wrapping_add(1);
21426                for (i, m) in members.iter().enumerate() {
21427                    self.retained_scc_members.insert(m.vertex, scc_id);
21428                    // §4 persistence snapshot, written once: retained
21429                    // members do not pass through `redirty_for_next_recalc`.
21430                    if matches!(last_value[i], LiteralValue::Empty) {
21431                        self.iterative_state_values.remove(&m.vertex);
21432                    } else {
21433                        self.iterative_state_values
21434                            .insert(m.vertex, last_value[i].clone());
21435                    }
21436                }
21437            } else {
21438                self.pending_iterative_redirty
21439                    .extend(members.iter().map(|m| m.vertex));
21440            }
21441        } else if !self.iterative_state_values.is_empty() {
21442            // The cycle dissolved (phantom settle or `#CIRC!` stamping):
21443            // these members are ordinary formulas again and must not carry
21444            // stale iteration state into a future cycle.
21445            for m in members.iter() {
21446                self.iterative_state_values.remove(&m.vertex);
21447            }
21448        }
21449
21450        {
21451            let t = &mut self.last_cycle_telemetry;
21452            t.static_sccs += 1;
21453            if witnessed_cycles == 0 && stamped == 0 && !capped {
21454                t.phantom_sccs += 1;
21455            }
21456            t.live_cycles_witnessed += witnessed_cycles;
21457            t.circ_cells_stamped += stamped;
21458            t.settle_passes_total += passes;
21459            t.max_passes_single_scc = t.max_passes_single_scc.max(passes);
21460            if iterating {
21461                t.iterated_sccs += 1;
21462                if converged {
21463                    t.converged_sccs += 1;
21464                }
21465                t.max_abs_delta_at_stop = t.max_abs_delta_at_stop.max(iter_max_delta);
21466                t.nan_converged += iter_nan_converged;
21467            }
21468            if capped {
21469                t.capped_sccs += 1;
21470            }
21471            t.elapsed_ms += task_start.elapsed().as_millis();
21472        }
21473
21474        Ok(stamped)
21475    }
21476
21477    /// Recorded sibling of [`Self::evaluate_vertex_immutable`]: evaluates one
21478    /// SCC member's AST via an [`Interpreter`] over a [`RecordingContext`] so
21479    /// reads that actually occur are captured as live edges. Value semantics
21480    /// must match `evaluate_vertex_immutable` exactly (including the missing-
21481    /// AST `Number(0.0)` quirk, G14); named Cell/Range/Literal definitions
21482    /// delegate to it after recording the definition region by hand (those
21483    /// reads bypass the context).
21484    fn evaluate_vertex_recorded(
21485        &self,
21486        vertex_id: VertexId,
21487        ctx: &RecordingContext<'_, R>,
21488        collector: &LiveEdgeCollector,
21489    ) -> Result<LiteralValue, ExcelError> {
21490        if !self.graph.vertex_exists(vertex_id) {
21491            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
21492                .with_message(format!("Vertex not found: {vertex_id:?}")));
21493        }
21494
21495        let kind = self.graph.get_vertex_kind(vertex_id);
21496        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
21497
21498        match kind {
21499            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
21500                let Some(view) = self.graph.formula_view(vertex_id) else {
21501                    return Ok(LiteralValue::Number(0.0)); // G14 quirk
21502                };
21503                let sheet_name = self.graph.sheet_name(sheet_id);
21504                let cell_ref = self
21505                    .graph
21506                    .get_cell_ref(vertex_id)
21507                    .expect("cell ref for vertex");
21508                let interpreter = Interpreter::new_with_cell(ctx, sheet_name, cell_ref)
21509                    .with_resolved_functions(&self.resolved_functions);
21510                interpreter
21511                    .evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
21512                    .map(|cv| {
21513                        let format = cv.format_id();
21514                        self.record_derived_format(vertex_id, format);
21515                        self.materialize_formula_result(vertex_id, cv)
21516                    })
21517                    .or_else(|error| self.fit_formula_error(vertex_id, error))
21518            }
21519            VertexKind::NamedScalar | VertexKind::NamedArray => {
21520                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
21521                    ExcelError::new(ExcelErrorKind::Name)
21522                        .with_message("Named range metadata missing".to_string())
21523                })?;
21524
21525                match &named_range.definition {
21526                    NamedDefinition::Formula { ast, .. } => {
21527                        let _origin = self
21528                            .operation_diagnostic_collector
21529                            .as_ref()
21530                            .map(|c| c.named_scope(vertex_id));
21531                        let context_sheet = match named_range.scope {
21532                            NameScope::Sheet(id) => id,
21533                            NameScope::Workbook => sheet_id,
21534                        };
21535                        let sheet_name = self.graph.sheet_name(context_sheet);
21536                        let cell_ref = self
21537                            .graph
21538                            .get_cell_ref(vertex_id)
21539                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
21540                        let interpreter = Interpreter::new_with_cell(ctx, sheet_name, cell_ref)
21541                            .with_resolved_functions(&self.resolved_functions);
21542                        if kind == VertexKind::NamedScalar {
21543                            interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
21544                        } else {
21545                            match interpreter.evaluate_ast(ast) {
21546                                Ok(cv) => match cv.into_literal() {
21547                                    v @ LiteralValue::Array(_) => Ok(v),
21548                                    other => Ok(LiteralValue::Array(vec![vec![other]])),
21549                                },
21550                                Err(err) => Ok(LiteralValue::Error(err)),
21551                            }
21552                        }
21553                    }
21554                    NamedDefinition::Cell(cell_ref) => {
21555                        // The definition is read via direct grid access in
21556                        // `evaluate_vertex_immutable`; record the live edge
21557                        // by hand before delegating.
21558                        collector.record_scalar(
21559                            cell_ref.sheet_id,
21560                            cell_ref.coord.row(),
21561                            cell_ref.coord.col(),
21562                        );
21563                        self.evaluate_vertex_immutable(vertex_id)
21564                    }
21565                    NamedDefinition::Range(range_ref) => {
21566                        if range_ref.start.sheet_id == range_ref.end.sheet_id {
21567                            collector.record_rect(
21568                                range_ref.start.sheet_id,
21569                                range_ref.start.coord.row(),
21570                                range_ref.start.coord.col(),
21571                                range_ref.end.coord.row(),
21572                                range_ref.end.coord.col(),
21573                            );
21574                        }
21575                        self.evaluate_vertex_immutable(vertex_id)
21576                    }
21577                    NamedDefinition::Literal(_) => self.evaluate_vertex_immutable(vertex_id),
21578                }
21579            }
21580            _ => self.evaluate_vertex_immutable(vertex_id),
21581        }
21582    }
21583
21584    /// Pending source occupancy is independent of formula preparation and value caches.
21585    fn pending_spill_occupied(&self, anchor: CellRef, end_row: u32, end_col: u32) -> bool {
21586        let sheet = self.graph.sheet_name(anchor.sheet_id);
21587        let package = self
21588            .staged_formulas
21589            .get(sheet)
21590            .and_then(|staged| staged.deferred_package.as_ref());
21591        self.staged_formula_index.occupies_spill(
21592            sheet,
21593            (anchor.coord.row() + 1, anchor.coord.col() + 1),
21594            (end_row + 1, end_col + 1),
21595            |point| package.is_some_and(|package| package.suppressed.contains(&point)),
21596        )
21597    }
21598
21599    fn remember_pending_spill(
21600        &mut self,
21601        vertex: VertexId,
21602        anchor: CellRef,
21603        region: Region,
21604    ) -> Result<(), ExcelError> {
21605        self.cancellation_checkpoint("pending spill occupancy")?;
21606        self.resource_checkpoint(1)?;
21607        if let Some(entry) = self
21608            .blocked_pending_spills
21609            .iter_mut()
21610            .find(|entry| entry.0 == vertex)
21611        {
21612            *entry = (vertex, anchor, region);
21613            return Ok(());
21614        }
21615        if self.blocked_pending_spills.len() == self.blocked_pending_spills.capacity() {
21616            // Geometric growth avoids copying every existing retry entry for
21617            // every new blocked anchor. Admit the entire capacity increment.
21618            let additional = self.blocked_pending_spills.capacity().max(1);
21619            let bytes =
21620                (additional as u64)
21621                    .saturating_mul(std::mem::size_of::<(VertexId, CellRef, Region)>() as u64);
21622            if let Some(ledger) = self.active_resource_ledger.as_mut() {
21623                ledger
21624                    .reserve_retained(bytes)
21625                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
21626                self.source_cache_accounted = self.source_cache_accounted.saturating_add(bytes);
21627            }
21628            if self
21629                .blocked_pending_spills
21630                .try_reserve_exact(additional)
21631                .is_err()
21632            {
21633                if let Some(ledger) = self.active_resource_ledger.as_mut() {
21634                    ledger
21635                        .release_retained(bytes)
21636                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
21637                    self.source_cache_accounted -= bytes;
21638                }
21639                return Err(crate::engine::ResourceLedgerError::Exhausted(
21640                    formualizer_common::ResourceExhaustionDetail {
21641                        reason: formualizer_common::ResourceExhaustionReason::RetainedMemory,
21642                        limit: u64::MAX,
21643                        observed: bytes,
21644                        request_id: None,
21645                    },
21646                )
21647                .into_excel_error());
21648            }
21649        }
21650        self.blocked_pending_spills.push((vertex, anchor, region));
21651        Ok(())
21652    }
21653
21654    // Successful edits wake only intersecting attempted regions. Keep the entry
21655    // until evaluation: logged edits may still roll back, and materializing a
21656    // pending formula must not destroy its anchor's occupancy retry information.
21657    fn invalidate_pending_spills(&mut self, scope: StructuralScope) {
21658        if let StructuralScope::RemovedSheet(sheet) = scope {
21659            self.blocked_pending_spills
21660                .retain(|entry| entry.1.sheet_id != sheet);
21661            return;
21662        }
21663        for &(vertex, anchor, region) in &self.blocked_pending_spills {
21664            let affected = match scope {
21665                StructuralScope::Cell { sheet, row, col } => {
21666                    region.intersects(&Region::point(sheet, row, col))
21667                }
21668                StructuralScope::Region(changed) => region.intersects(&changed),
21669                StructuralScope::Sheet(sheet) | StructuralScope::RemovedSheet(sheet) => {
21670                    region.sheet_id() == sheet
21671                }
21672                StructuralScope::OpaqueGlobal | StructuralScope::AllSheets => true,
21673            };
21674            if affected
21675                && self.graph.vertex_exists(vertex)
21676                && self.graph.get_cell_ref(vertex) == Some(anchor)
21677                && matches!(
21678                    self.graph.get_vertex_kind(vertex),
21679                    VertexKind::FormulaScalar | VertexKind::FormulaArray
21680                )
21681            {
21682                self.graph.mark_vertex_dirty(vertex);
21683            }
21684        }
21685    }
21686
21687    fn guard_pending_spill_commit(
21688        &mut self,
21689        anchor_vertex: VertexId,
21690        targets: &[CellRef],
21691    ) -> Result<(), ExcelError> {
21692        let Some(anchor) = self.graph.get_cell_ref(anchor_vertex) else {
21693            return Ok(());
21694        };
21695        let Some(last) = targets.last() else {
21696            return Ok(());
21697        };
21698        let occupied = self.pending_spill_occupied(anchor, last.coord.row(), last.coord.col())
21699            || self
21700                .graph
21701                .table_intersects_spill(anchor, last.coord.row(), last.coord.col());
21702        if (occupied
21703            || self
21704                .blocked_pending_spills
21705                .iter()
21706                .any(|entry| entry.0 == anchor_vertex))
21707            && let Err(error) = self.remember_pending_spill(
21708                anchor_vertex,
21709                anchor,
21710                Region::rect(
21711                    anchor.sheet_id,
21712                    anchor.coord.row(),
21713                    last.coord.row(),
21714                    anchor.coord.col(),
21715                    last.coord.col(),
21716                ),
21717            )
21718        {
21719            self.spill_mgr.release_owner(anchor_vertex);
21720            return Err(error);
21721        }
21722        if occupied {
21723            self.spill_mgr.release_owner(anchor_vertex);
21724            return Err(ExcelError::new(ExcelErrorKind::Spill)
21725                .with_message("Spill blocked")
21726                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
21727                    expected_rows: last.coord.row() - anchor.coord.row() + 1,
21728                    expected_cols: last.coord.col() - anchor.coord.col() + 1,
21729                }));
21730        }
21731        Ok(())
21732    }
21733
21734    /// Commit spill via shim and mirror resulting cells into Arrow overlay.
21735    fn commit_spill_and_mirror(
21736        &mut self,
21737        anchor_vertex: VertexId,
21738        targets: &[CellRef],
21739        rows: Vec<Vec<LiteralValue>>,
21740        delta: Option<&mut DeltaCollector>,
21741    ) -> Result<(), ExcelError> {
21742        self.guard_pending_spill_commit(anchor_vertex, targets)?;
21743        let prev_spill_cells = self.graph.spill_cells_to_clear(anchor_vertex);
21744
21745        if let Some(delta) = delta
21746            && delta.mode != DeltaMode::Off
21747        {
21748            let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
21749                targets.iter().copied().collect();
21750            let empty = LiteralValue::Empty;
21751
21752            // Clears (prev - targets)
21753            for cell in prev_spill_cells.iter() {
21754                if target_set.contains(cell) {
21755                    continue;
21756                }
21757                let sheet_name = self.graph.sheet_name(cell.sheet_id);
21758                let old = self
21759                    .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
21760                    .unwrap_or(LiteralValue::Empty);
21761                if old != empty {
21762                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
21763                }
21764            }
21765
21766            // Writes (targets)
21767            if !targets.is_empty() && !rows.is_empty() && !rows[0].is_empty() {
21768                let width = rows[0].len();
21769                for (idx, cell) in targets.iter().enumerate() {
21770                    let r_off = idx / width;
21771                    let c_off = idx % width;
21772                    let new = rows
21773                        .get(r_off)
21774                        .and_then(|r| r.get(c_off))
21775                        .cloned()
21776                        .unwrap_or(LiteralValue::Empty);
21777                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
21778                    let old = self
21779                        .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
21780                        .unwrap_or(LiteralValue::Empty);
21781                    if old != new {
21782                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
21783                    }
21784                }
21785            } else {
21786                // Degenerate shapes: if we have targets but no rows, treat as writing Empty.
21787                for cell in targets.iter() {
21788                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
21789                    let old = self
21790                        .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
21791                        .unwrap_or(LiteralValue::Empty);
21792                    if !matches!(old, LiteralValue::Empty) {
21793                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
21794                    }
21795                }
21796            }
21797        }
21798
21799        // Commit via shim (releases locks). When the graph value cache is disabled (Arrow-canonical
21800        // values), plan/commit must consult Arrow storage to detect non-empty value blockers.
21801        let arrow_sheets = &self.arrow_sheets;
21802        self.spill_mgr.commit_array_with_value_probe(
21803            &mut self.graph,
21804            anchor_vertex,
21805            targets,
21806            rows.clone(),
21807            |g, cell| {
21808                let sheet_name = g.sheet_name(cell.sheet_id);
21809                let asheet = arrow_sheets.sheet(sheet_name)?;
21810                let r0 = cell.coord.row() as usize;
21811                let c0 = cell.coord.col() as usize;
21812                let v = asheet.get_cell_value(r0, c0);
21813                if matches!(v, LiteralValue::Empty) {
21814                    None
21815                } else {
21816                    Some(v)
21817                }
21818            },
21819        )?;
21820
21821        self.freshness_note_spill_change(&prev_spill_cells, targets);
21822        self.blocked_pending_spills
21823            .retain(|entry| entry.0 != anchor_vertex);
21824        self.spill_blocker_waiters.forget(anchor_vertex);
21825        if let Some(scope) = Self::structural_scope_from_cells(&prev_spill_cells) {
21826            self.record_structural_change(scope);
21827        }
21828        if let Some(scope) = Self::structural_scope_from_cells(targets) {
21829            self.record_structural_change(scope);
21830        }
21831
21832        if self.config.arrow_storage_enabled
21833            && self.config.delta_overlay_enabled
21834            && self.config.write_formula_overlay_enabled
21835        {
21836            if !prev_spill_cells.is_empty() {
21837                let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
21838                    targets.iter().copied().collect();
21839                let empty = LiteralValue::Empty;
21840                for cell in prev_spill_cells.iter() {
21841                    if !target_set.contains(cell) {
21842                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
21843                        self.mirror_value_to_computed_overlay(
21844                            &sheet_name,
21845                            cell.coord.row() + 1,
21846                            cell.coord.col() + 1,
21847                            &empty,
21848                        );
21849                    }
21850                }
21851            }
21852
21853            for (idx, cell) in targets.iter().enumerate() {
21854                if rows.is_empty() || rows[0].is_empty() {
21855                    break;
21856                }
21857                let width = rows[0].len();
21858                let r_off = idx / width;
21859                let c_off = idx % width;
21860                let v = rows[r_off][c_off].clone();
21861                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
21862                self.mirror_value_to_computed_overlay(
21863                    &sheet_name,
21864                    cell.coord.row() + 1,
21865                    cell.coord.col() + 1,
21866                    &v,
21867                );
21868            }
21869        }
21870        Ok(())
21871    }
21872}
21873
21874#[cfg(test)]
21875#[path = "tests/authority_schedule_execution.rs"]
21876mod authority_schedule_execution;
21877
21878#[cfg(test)]
21879#[path = "tests/scalar_commit.rs"]
21880mod scalar_commit_tests;
21881
21882#[cfg(test)]
21883#[path = "tests/observed_effect_identity.rs"]
21884mod observed_effect_identity;
21885
21886#[cfg(test)]
21887#[path = "tests/pending_spill.rs"]
21888mod pending_spill_tests;
21889
21890#[cfg(test)]
21891#[path = "tests/spill_batch_abort_192.rs"]
21892mod spill_batch_abort_192;
21893
21894#[cfg(test)]
21895#[path = "tests/observed_future_spill_329.rs"]
21896mod observed_future_spill_329;
21897
21898// ── Effects pipeline (ticket 603) ──────────────────────────────────────────
21899//
21900// Compute → Plan → Apply separation for evaluation side-effects.
21901
21902use crate::engine::effects::Effect;
21903
21904// Scalar writes and spill clears stay inline; larger spill plans grow normally.
21905type PlannedEffects = smallvec::SmallVec<[Effect; 2]>;
21906use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog, SpillSnapshot};
21907
21908impl<R> Engine<R>
21909where
21910    R: EvaluationContext,
21911{
21912    /// Plan effects for a single vertex after its value has been computed.
21913    ///
21914    /// This reads graph state but only performs lightweight mutations
21915    /// (`set_kind`, `spill_mgr.reserve`) that are needed for correctness
21916    /// during the planning phase.  Value-changing mutations are deferred to
21917    /// `apply_effect`.
21918    pub(crate) fn plan_vertex_effects(
21919        &mut self,
21920        vertex_id: VertexId,
21921        computed_value: LiteralValue,
21922    ) -> Result<PlannedEffects, ExcelError> {
21923        // FR3: a stale dynamic reader publishes nothing; FR2: anything else
21924        // leaves the dirty set at its commit (design §8.2).
21925        if self.freshness_armed() {
21926            if self.freshness_drop_stale(vertex_id) {
21927                return Ok(PlannedEffects::new());
21928            }
21929            let mut effects = self.plan_vertex_effects_unrecorded(vertex_id, computed_value)?;
21930            self.freshness_bind_planned_result(vertex_id, &mut effects);
21931            self.freshness_mark_committed(vertex_id);
21932            return Ok(effects);
21933        }
21934        self.plan_vertex_effects_unrecorded(vertex_id, computed_value)
21935    }
21936
21937    fn plan_vertex_effects_unrecorded(
21938        &mut self,
21939        vertex_id: VertexId,
21940        computed_value: LiteralValue,
21941    ) -> Result<PlannedEffects, ExcelError> {
21942        let kind = self.graph.get_vertex_kind(vertex_id);
21943        let is_formula = matches!(kind, VertexKind::FormulaScalar | VertexKind::FormulaArray);
21944
21945        // If this vertex's cell is currently covered by a spill from a different
21946        // anchor, ignore the computed result.  Formula vertices are exempt:
21947        // they must still evaluate so that overlapping spills produce #SPILL!.
21948        if !is_formula {
21949            if let Some(cell) = self.graph.get_cell_ref(vertex_id)
21950                && let Some(owner) = self.graph.spill_registry_anchor_for_cell(cell)
21951                && owner != vertex_id
21952            {
21953                return Ok(PlannedEffects::new());
21954            }
21955            // Non-formula vertices: store value as-is (arrays remain arrays; no spill).
21956            return Ok(smallvec::smallvec![Effect::WriteCell {
21957                vertex_id,
21958                value: computed_value,
21959            }]);
21960        }
21961
21962        match computed_value {
21963            LiteralValue::Array(rows) => self.plan_array_effects(vertex_id, rows),
21964            other => self.plan_scalar_effects(vertex_id, other),
21965        }
21966    }
21967
21968    /// Plan effects for a formula vertex that produced a scalar/error result.
21969    fn plan_scalar_effects(
21970        &mut self,
21971        vertex_id: VertexId,
21972        value: LiteralValue,
21973    ) -> Result<PlannedEffects, ExcelError> {
21974        // Range admission substitutes the same cap error before allocating rows.
21975        // Preserve the owned-array rejection's kind, release and clear effects.
21976        if let LiteralValue::Error(error) = &value
21977            && error.kind == ExcelErrorKind::Spill
21978            && error.message() == Some("SpillTooLarge")
21979            && let formualizer_common::ExcelErrorExtra::Spill {
21980                expected_rows,
21981                expected_cols,
21982            } = error.extra()
21983            && u64::from(*expected_rows).saturating_mul(u64::from(*expected_cols))
21984                > u64::from(self.config.spill.max_spill_cells)
21985        {
21986            self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
21987            return self.plan_spill_error_effects(
21988                vertex_id,
21989                "SpillTooLarge",
21990                *expected_rows,
21991                *expected_cols,
21992            );
21993        }
21994        if !matches!(&value, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Spill) {
21995            self.blocked_pending_spills
21996                .retain(|entry| entry.0 != vertex_id);
21997            self.spill_blocker_waiters.forget(vertex_id);
21998        }
21999        let has_spill = self
22000            .graph
22001            .spill_cells_for_anchor(vertex_id)
22002            .is_some_and(|c| !c.is_empty());
22003
22004        let mut effects = PlannedEffects::new();
22005        if has_spill {
22006            effects.push(Effect::SpillClear {
22007                anchor_vertex: vertex_id,
22008            });
22009        }
22010        effects.push(Effect::WriteCell { vertex_id, value });
22011        Ok(effects)
22012    }
22013
22014    /// Plan effects for a formula vertex that produced an array result.
22015    fn plan_array_effects(
22016        &mut self,
22017        vertex_id: VertexId,
22018        rows: Vec<Vec<LiteralValue>>,
22019    ) -> Result<PlannedEffects, ExcelError> {
22020        // Lightweight mutation needed for correct spill-blocking checks.
22021        self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
22022
22023        let anchor = self
22024            .graph
22025            .get_cell_ref(vertex_id)
22026            .expect("cell ref for vertex");
22027        let sheet_id = anchor.sheet_id;
22028        let h = rows.len() as u32;
22029        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
22030
22031        // Hard cap to avoid vertex explosion from huge dynamic arrays.
22032        let spill_cells = (h as u64).saturating_mul(w as u64);
22033        if spill_cells > self.config.spill.max_spill_cells as u64 {
22034            return self.plan_spill_error_effects(vertex_id, "SpillTooLarge", h, w);
22035        }
22036
22037        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity).
22038        const PACKED_MAX_ROW: u32 = 1_048_575;
22039        const PACKED_MAX_COL: u32 = 16_383;
22040        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
22041        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
22042        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
22043            return self.plan_spill_error_effects(vertex_id, "Spill exceeds sheet bounds", h, w);
22044        }
22045
22046        if h != 0 && w != 0 && self.graph.table_intersects_spill(anchor, end_row, end_col) {
22047            return self.plan_spill_error_effects(vertex_id, "Spill blocked by table", h, w);
22048        }
22049        let mut targets = Vec::new();
22050        for r in 0..h {
22051            for c in 0..w {
22052                targets.push(self.graph.make_cell_ref_internal(
22053                    sheet_id,
22054                    anchor.coord.row() + r,
22055                    anchor.coord.col() + c,
22056                ));
22057            }
22058        }
22059
22060        if h != 0 && w != 0 {
22061            let occupied = self.pending_spill_occupied(anchor, end_row, end_col);
22062            if occupied
22063                || self
22064                    .blocked_pending_spills
22065                    .iter()
22066                    .any(|entry| entry.0 == vertex_id)
22067            {
22068                self.spill_mgr.release_owner(vertex_id);
22069                self.remember_pending_spill(
22070                    vertex_id,
22071                    anchor,
22072                    Region::rect(
22073                        sheet_id,
22074                        anchor.coord.row(),
22075                        end_row,
22076                        anchor.coord.col(),
22077                        end_col,
22078                    ),
22079                )?;
22080            }
22081            if occupied {
22082                return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
22083            }
22084        }
22085
22086        // Region lock via spill manager.
22087        match self.spill_mgr.reserve(
22088            vertex_id,
22089            anchor,
22090            SpillShape { rows: h, cols: w },
22091            SpillMeta {
22092                epoch: self.recalc_epoch,
22093                config: self.config.spill,
22094            },
22095        ) {
22096            Ok(()) => {
22097                // Validate spill region is available.
22098                let mut yield_effects = PlannedEffects::new();
22099                if let Err((e, blocker)) = self
22100                    .graph
22101                    .plan_spill_region_with_blocker(vertex_id, &targets)
22102                {
22103                    let yielding = if e.message() == Some("BlockedBySpill") {
22104                        self.graph.spills_yielding_to(vertex_id, &targets)
22105                    } else {
22106                        None
22107                    };
22108                    if let Some(yielding) = yielding {
22109                        // A colliding spill later in (sheet, column, row)
22110                        // order yields: it is cleared and replans to
22111                        // `#SPILL!` with its dependents.
22112                        for &other in &yielding {
22113                            let (rows, cols) = self
22114                                .graph
22115                                .spill_extent_for_anchor(other)
22116                                .map(|(first, last)| {
22117                                    (
22118                                        last.coord.row() - first.coord.row() + 1,
22119                                        last.coord.col() - first.coord.col() + 1,
22120                                    )
22121                                })
22122                                .unwrap_or((0, 0));
22123                            yield_effects.push(Effect::SpillClear {
22124                                anchor_vertex: other,
22125                            });
22126                            yield_effects.push(Effect::WriteCell {
22127                                vertex_id: other,
22128                                value: Self::spill_error_value("Spill blocked", rows, cols),
22129                            });
22130                        }
22131                        self.graph.mark_dirty_many(&yielding);
22132                    } else {
22133                        // A formula or spill blocker can go away without this
22134                        // anchor recomputing: remember the attempted region so
22135                        // an edit inside it wakes the anchor.
22136                        if matches!(e.message(), Some("BlockedByFormula" | "BlockedBySpill")) {
22137                            self.spill_blocker_waiters.wait(vertex_id, blocker);
22138                        }
22139                        return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
22140                    }
22141                }
22142
22143                // Arrow-canonical mode: graph planning cannot see non-empty value blockers because
22144                // cell values are not cached in the dependency graph. Consult Arrow storage to
22145                // detect occupied cells in the target region.
22146                if !self.graph.value_cache_enabled() {
22147                    let sheet_name = self.graph.sheet_name(sheet_id);
22148                    if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
22149                        for cell in targets.iter() {
22150                            // Allow overwriting the anchor itself.
22151                            if *cell == anchor {
22152                                continue;
22153                            }
22154                            // Allow cells already owned by a spill (plan() validated spill ownership).
22155                            if self.graph.spill_registry_anchor_for_cell(*cell).is_some() {
22156                                continue;
22157                            }
22158                            // Skip formula blockers; plan() handled them (or allowed).
22159                            if let Some(vid) = self.graph.get_vertex_id_for_address(cell)
22160                                && vid != vertex_id
22161                            {
22162                                match self.graph.get_vertex_kind(vid) {
22163                                    VertexKind::FormulaScalar | VertexKind::FormulaArray => {
22164                                        continue;
22165                                    }
22166                                    _ => {}
22167                                }
22168                            }
22169
22170                            let v = asheet.get_cell_value(
22171                                cell.coord.row() as usize,
22172                                cell.coord.col() as usize,
22173                            );
22174                            if !matches!(v, LiteralValue::Empty) {
22175                                // A literal blocker can disappear without
22176                                // another precedent edit. Wake only its anchor.
22177                                self.spill_blocker_waiters.wait(vertex_id, *cell);
22178                                return self.plan_spill_error_effects(
22179                                    vertex_id,
22180                                    "BlockedByValue",
22181                                    h,
22182                                    w,
22183                                );
22184                            }
22185                        }
22186                    }
22187                }
22188
22189                let top_left = rows
22190                    .first()
22191                    .and_then(|r| r.first())
22192                    .cloned()
22193                    .unwrap_or(LiteralValue::Empty);
22194
22195                let mut effects = yield_effects;
22196                // Clear previous spill if any.
22197                let has_prev = self
22198                    .graph
22199                    .spill_cells_for_anchor(vertex_id)
22200                    .is_some_and(|c| !c.is_empty());
22201                if has_prev {
22202                    effects.push(Effect::SpillClear {
22203                        anchor_vertex: vertex_id,
22204                    });
22205                }
22206                effects.push(Effect::SpillCommit {
22207                    anchor_vertex: vertex_id,
22208                    anchor_cell: anchor,
22209                    target_cells: targets,
22210                    values: rows,
22211                });
22212                effects.push(Effect::WriteCell {
22213                    vertex_id,
22214                    value: top_left,
22215                });
22216                Ok(effects)
22217            }
22218            Err(e) => {
22219                let msg = e.message().unwrap_or("Spill blocked");
22220                self.plan_spill_error_effects(vertex_id, msg, h, w)
22221            }
22222        }
22223    }
22224
22225    /// The `#SPILL!` value a blocked anchor holds.
22226    fn spill_error_value(message: &str, expected_rows: u32, expected_cols: u32) -> LiteralValue {
22227        LiteralValue::Error(
22228            ExcelError::new(ExcelErrorKind::Spill)
22229                .with_message(message)
22230                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
22231                    expected_rows,
22232                    expected_cols,
22233                }),
22234        )
22235    }
22236
22237    /// Build the effect list for a spill that failed validation.
22238    fn plan_spill_error_effects(
22239        &mut self,
22240        vertex_id: VertexId,
22241        message: &str,
22242        expected_rows: u32,
22243        expected_cols: u32,
22244    ) -> Result<PlannedEffects, ExcelError> {
22245        self.spill_mgr.release_owner(vertex_id);
22246        let spill_val = Self::spill_error_value(message, expected_rows, expected_cols);
22247
22248        let effects = smallvec::smallvec![
22249            Effect::SpillClear {
22250                anchor_vertex: vertex_id,
22251            },
22252            Effect::WriteCell {
22253                vertex_id,
22254                value: spill_val,
22255            },
22256        ];
22257        Ok(effects)
22258    }
22259
22260    /// Apply a single effect, performing the actual graph mutations.
22261    pub(crate) fn apply_effect(
22262        &mut self,
22263        effect: &Effect,
22264        delta: Option<&mut DeltaCollector>,
22265        log: Option<&mut ChangeLog>,
22266    ) -> Result<(), ExcelError> {
22267        self.apply_effect_with_computed_writes(effect, delta, log, None)
22268    }
22269
22270    fn apply_effect_with_computed_writes(
22271        &mut self,
22272        effect: &Effect,
22273        delta: Option<&mut DeltaCollector>,
22274        log: Option<&mut ChangeLog>,
22275        mut computed_writes: Option<&mut ComputedWriteBuffer>,
22276    ) -> Result<(), ExcelError> {
22277        match effect {
22278            Effect::WriteCell { vertex_id, value } => {
22279                self.apply_write_cell(*vertex_id, value, delta, computed_writes)?;
22280            }
22281            Effect::WriteRecordedCell { result } => {
22282                let vertex = result.vertex();
22283                let disposition = self.apply_write_cell(
22284                    vertex,
22285                    result.value(),
22286                    delta,
22287                    computed_writes.as_deref_mut(),
22288                )?;
22289                match disposition {
22290                    OverlayWriteDisposition::Installed => {
22291                        self.freshness_record_installed(result, true)
22292                    }
22293                    OverlayWriteDisposition::Skipped => self.freshness_record_installed(
22294                        result,
22295                        self.graph.get_cell_ref(vertex).is_none(),
22296                    ),
22297                    OverlayWriteDisposition::Buffered => {
22298                        let buffer = computed_writes.expect("buffered write has its buffer");
22299                        let cell = self.graph.get_cell_ref(vertex).expect("buffered grid cell");
22300                        let seq = buffer.writes.last().expect("staged write").seq();
22301                        buffer.read_claims.insert(
22302                            (cell.sheet_id, cell.coord.row(), cell.coord.col()),
22303                            BufferedReadClaim {
22304                                seq,
22305                                result: result.clone(),
22306                                winning: false,
22307                            },
22308                        );
22309                    }
22310                }
22311            }
22312            Effect::SpillClear { anchor_vertex } => {
22313                self.apply_spill_clear(*anchor_vertex, delta, log, computed_writes)?;
22314            }
22315            Effect::SpillCommit {
22316                anchor_vertex,
22317                anchor_cell: _,
22318                target_cells,
22319                values,
22320            } => {
22321                self.apply_spill_commit(
22322                    *anchor_vertex,
22323                    target_cells,
22324                    values.clone(),
22325                    delta,
22326                    log,
22327                    computed_writes,
22328                )?;
22329            }
22330        }
22331        Ok(())
22332    }
22333
22334    /// Apply a WriteCell effect. Read proof is promoted by the recorded-result
22335    /// caller only when this reports installation, never on mere enqueue.
22336    fn apply_write_cell(
22337        &mut self,
22338        vertex_id: VertexId,
22339        value: &LiteralValue,
22340        delta: Option<&mut DeltaCollector>,
22341        mut computed_writes: Option<&mut ComputedWriteBuffer>,
22342    ) -> Result<OverlayWriteDisposition, ExcelError> {
22343        if let Some(d) = delta
22344            && d.mode != DeltaMode::Off
22345        {
22346            if let Some(buffer) = computed_writes.as_deref_mut() {
22347                self.flush_computed_write_buffer(buffer)?;
22348            }
22349            if let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id) {
22350                let sheet_name = self.graph.sheet_name(cell.sheet_id);
22351                let old = self
22352                    .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
22353                    .unwrap_or(LiteralValue::Empty);
22354                if old != *value {
22355                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
22356                }
22357            }
22358        }
22359        self.graph.update_vertex_value_ref(vertex_id, value);
22360        self.record_vertex_value_to_overlay(vertex_id, value, computed_writes)
22361    }
22362
22363    /// Apply a SpillClear effect.
22364    fn apply_spill_clear(
22365        &mut self,
22366        anchor_vertex: VertexId,
22367        delta: Option<&mut DeltaCollector>,
22368        log: Option<&mut ChangeLog>,
22369        computed_writes: Option<&mut ComputedWriteBuffer>,
22370    ) -> Result<(), ExcelError> {
22371        if let Some(buffer) = computed_writes {
22372            self.flush_computed_write_buffer(buffer)?;
22373        }
22374
22375        let spill_cells = self.graph.spill_cells_to_clear(anchor_vertex);
22376        if spill_cells.is_empty() {
22377            return Ok(());
22378        }
22379        self.graph
22380            .authority_host_mut()
22381            .invalidate_observed_value(anchor_vertex);
22382
22383        // Snapshot for ChangeLog before clearing.
22384        let snapshot = if log.is_some() {
22385            self.snapshot_spill_for_anchor(anchor_vertex)
22386        } else {
22387            None
22388        };
22389
22390        // Record delta for cleared cells.
22391        if let Some(d) = delta
22392            && d.mode != DeltaMode::Off
22393        {
22394            let empty = LiteralValue::Empty;
22395            for cell in spill_cells.iter() {
22396                let sheet_name = self.graph.sheet_name(cell.sheet_id);
22397                let old = self
22398                    .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
22399                    .unwrap_or(LiteralValue::Empty);
22400                if old != empty {
22401                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
22402                }
22403            }
22404        }
22405
22406        self.graph.clear_spill_region(anchor_vertex);
22407        self.freshness_note_spill_change(&spill_cells, &[]);
22408        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
22409            self.record_structural_change(scope);
22410        }
22411
22412        // Mirror Empty to Arrow overlay for cleared cells.
22413        if self.config.arrow_storage_enabled
22414            && self.config.delta_overlay_enabled
22415            && self.config.write_formula_overlay_enabled
22416        {
22417            let empty = LiteralValue::Empty;
22418            for cell in spill_cells.iter() {
22419                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
22420                self.mirror_value_to_computed_overlay(
22421                    &sheet_name,
22422                    cell.coord.row() + 1,
22423                    cell.coord.col() + 1,
22424                    &empty,
22425                );
22426            }
22427        }
22428
22429        // ChangeLog.
22430        if let Some(log) = log
22431            && let Some(old) = snapshot
22432        {
22433            log.record(ChangeEvent::SpillCleared {
22434                anchor: anchor_vertex,
22435                old,
22436            });
22437        }
22438        Ok(())
22439    }
22440
22441    /// Apply a SpillCommit effect.
22442    fn apply_spill_commit(
22443        &mut self,
22444        anchor_vertex: VertexId,
22445        target_cells: &[CellRef],
22446        values: Vec<Vec<LiteralValue>>,
22447        delta: Option<&mut DeltaCollector>,
22448        log: Option<&mut ChangeLog>,
22449        computed_writes: Option<&mut ComputedWriteBuffer>,
22450    ) -> Result<(), ExcelError> {
22451        self.guard_pending_spill_commit(anchor_vertex, target_cells)?;
22452        if let Some(buffer) = computed_writes {
22453            self.flush_computed_write_buffer(buffer)?;
22454        }
22455
22456        // Snapshot for ChangeLog before commit.
22457        let old_snapshot = if log.is_some() {
22458            self.snapshot_spill_for_anchor(anchor_vertex)
22459        } else {
22460            None
22461        };
22462
22463        // Delegate to existing commit_spill_and_mirror for delta + overlay logic.
22464        self.commit_spill_and_mirror(anchor_vertex, target_cells, values.clone(), delta)?;
22465
22466        // ChangeLog.
22467        if let Some(log) = log {
22468            log.record(ChangeEvent::SpillCommitted {
22469                anchor: anchor_vertex,
22470                old: old_snapshot,
22471                new: SpillSnapshot {
22472                    target_cells: target_cells.to_vec(),
22473                    values,
22474                },
22475            });
22476        }
22477        Ok(())
22478    }
22479
22480    /// Snapshot a spill region for ChangeLog recording.
22481    ///
22482    /// Extracted from `VertexEditor::snapshot_spill_for_anchor` to be usable
22483    /// without creating a `VertexEditor`.
22484    fn snapshot_spill_for_anchor(&self, anchor: VertexId) -> Option<SpillSnapshot> {
22485        let cells = self.graph.spill_cells_for_anchor(anchor)?.to_vec();
22486        if cells.is_empty() {
22487            return None;
22488        }
22489
22490        let max = self.config.spill.max_spill_cells as usize;
22491        let mut cells = cells;
22492        if cells.len() > max {
22493            cells.truncate(max);
22494        }
22495
22496        let first = *cells.first().expect("non-empty spill cells");
22497        let sheet_name = self.graph.sheet_name(first.sheet_id).to_string();
22498        let row0 = first.coord.row();
22499        let col0 = first.coord.col();
22500
22501        let mut max_row = row0;
22502        let mut max_col = col0;
22503        let mut by_coord: FxHashMap<(u32, u32), LiteralValue> = FxHashMap::default();
22504        for cell in &cells {
22505            max_row = max_row.max(cell.coord.row());
22506            max_col = max_col.max(cell.coord.col());
22507            let v = self
22508                .read_cell_value(&sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
22509                .unwrap_or(LiteralValue::Empty);
22510            by_coord.insert((cell.coord.row(), cell.coord.col()), v);
22511        }
22512
22513        let rows = (max_row - row0 + 1) as usize;
22514        let cols = (max_col - col0 + 1) as usize;
22515        let mut values: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
22516        for r in 0..rows {
22517            let mut row: Vec<LiteralValue> = Vec::with_capacity(cols);
22518            for c in 0..cols {
22519                row.push(
22520                    by_coord
22521                        .get(&(row0 + r as u32, col0 + c as u32))
22522                        .cloned()
22523                        .unwrap_or(LiteralValue::Empty),
22524                );
22525            }
22526            values.push(row);
22527        }
22528
22529        if self.graph.spill_may_be_intruded(anchor) {
22530            cells.retain(|cell| {
22531                self.graph.spill_registry_anchor_for_cell(*cell) == Some(anchor)
22532                    && !self.graph.is_foreign_formula_cell(cell, anchor)
22533            });
22534        }
22535        Some(SpillSnapshot {
22536            target_cells: cells,
22537            values,
22538        })
22539    }
22540
22541    fn flush_before_range_dependent_vertex(
22542        &mut self,
22543        vertex_id: VertexId,
22544        computed_writes: &mut ComputedWriteBuffer,
22545    ) -> Result<(), ExcelError> {
22546        if self.graph.reads_compressed_range(vertex_id) {
22547            self.flush_computed_write_buffer(computed_writes)?;
22548        }
22549        Ok(())
22550    }
22551
22552    fn plan_vertex_effects_with_computed_flush(
22553        &mut self,
22554        vertex_id: VertexId,
22555        computed_value: LiteralValue,
22556        computed_writes: &mut ComputedWriteBuffer,
22557    ) -> Result<PlannedEffects, ExcelError> {
22558        if matches!(&computed_value, LiteralValue::Array(_)) {
22559            self.flush_computed_write_buffer(computed_writes)?;
22560        }
22561        self.plan_vertex_effects(vertex_id, computed_value)
22562    }
22563
22564    // ── Layer evaluation via effects pipeline ──────────────────────────────
22565
22566    fn evaluate_small_layer_direct_effects(
22567        &mut self,
22568        layer: &super::scheduler::Layer,
22569        delta: Option<&mut DeltaCollector>,
22570        log: Option<&mut ChangeLog>,
22571        cancel_flag: Option<&AtomicBool>,
22572        cancel_check_every: usize,
22573        cancel_message: &'static str,
22574    ) -> Result<usize, ExcelError> {
22575        let cancel = cancel_flag.map(|flag| (flag, cancel_check_every, cancel_message));
22576        self.evaluate_layer_units(layer, delta, log, cancel, false)
22577    }
22578
22579    /// Sequential layer walk over its units (single cells and family runs):
22580    /// each unit evaluates, then its vertices' effects apply in order. With
22581    /// `buffered`, computed writes coalesce in a layer buffer (flushed before
22582    /// a unit that reads a compressed range, before array results, and at
22583    /// the end); otherwise they apply directly. `cancel` = (flag, check every
22584    /// N vertices, message).
22585    fn evaluate_layer_units(
22586        &mut self,
22587        layer: &super::scheduler::Layer,
22588        delta: Option<&mut DeltaCollector>,
22589        log: Option<&mut ChangeLog>,
22590        cancel: Option<(&AtomicBool, usize, &'static str)>,
22591        buffered: bool,
22592    ) -> Result<usize, ExcelError> {
22593        self.evaluate_layer_units_until(layer, delta, log, cancel, buffered, None)
22594    }
22595
22596    /// [`Self::evaluate_layer_units`] that stops before the next unit once
22597    /// `stop_at` has passed; returns the vertices evaluated (a prefix of
22598    /// the layer, all committed).
22599    fn evaluate_layer_units_until(
22600        &mut self,
22601        layer: &super::scheduler::Layer,
22602        mut delta: Option<&mut DeltaCollector>,
22603        mut log: Option<&mut ChangeLog>,
22604        cancel: Option<(&AtomicBool, usize, &'static str)>,
22605        buffered: bool,
22606        stop_at: Option<crate::instant::FzInstant>,
22607    ) -> Result<usize, ExcelError> {
22608        // A chain unit: its run through the chain lift, or else cell by
22609        // cell in row order, each written before the next reads it.
22610        let mut chain_values = None;
22611        if layer.sequential && !layer.runs.is_empty() {
22612            chain_values = match layer.runs.as_slice() {
22613                [run] if run.start == 0 && run.len as usize == layer.vertices.len() => {
22614                    self.try_chain_lift(*run, &layer.vertices)
22615                }
22616                _ => None,
22617            };
22618            if chain_values.is_none() {
22619                let cells = super::scheduler::Layer {
22620                    vertices: layer.vertices.clone(),
22621                    runs: Vec::new(),
22622                    sequential: true,
22623                };
22624                return self.evaluate_layer_units_until(&cells, delta, log, cancel, false, stop_at);
22625            }
22626        }
22627        let chained = chain_values.is_some();
22628        // The chain lift computed every member: one block write, as a run.
22629        let buffered = buffered || chained;
22630        // A dynamic reader's targets are not always ordered before it (its
22631        // pre-probe or observed reads can miss them, e.g. after a structural
22632        // edit). In a buffered layer a member's dirty flag is cleared at its
22633        // commit but its value written at the flush: the members committed
22634        // since the last flush count as dirty for the reader's freshness
22635        // check, which then re-plans it after them.
22636        let track_unflushed = buffered
22637            && self.freshness_armed()
22638            && layer.vertices.iter().any(|&v| self.graph.is_dynamic(v));
22639        let mut committed_unit: &[VertexId] = &[];
22640        let mut computed_writes = ComputedWriteBuffer::default();
22641        let mut next_check = 0usize;
22642        let mut done = 0usize;
22643        for unit in layer_units(layer) {
22644            if done > 0
22645                && let Some(stop_at) = stop_at
22646                && crate::instant::FzInstant::now() >= stop_at
22647            {
22648                self.flush_computed_write_buffer(&mut computed_writes)?;
22649                if track_unflushed {
22650                    self.freshness_flushed();
22651                }
22652                return Ok(done);
22653            }
22654            if let Some((flag, every, message)) = cancel
22655                && every > 0
22656                && done >= next_check
22657            {
22658                next_check = (done / every + 1) * every;
22659                if flag.load(Ordering::Relaxed) {
22660                    if buffered {
22661                        self.flush_computed_write_buffer(&mut computed_writes)?;
22662                    }
22663                    return Err(ExcelError::new(ExcelErrorKind::Cancelled)
22664                        .with_message(message.to_string()));
22665                }
22666            }
22667            if buffered && self.unit_reads_compressed_range(layer, unit) {
22668                self.flush_computed_write_buffer(&mut computed_writes)?;
22669            }
22670            // The previous unit's members are committed (dirty flags
22671            // cleared); while their values wait in the buffer, a dynamic
22672            // reader's read of them is stale (`freshness_dirty_reads`).
22673            if track_unflushed {
22674                if computed_writes.is_empty() {
22675                    self.freshness_flushed();
22676                } else {
22677                    self.freshness_note_unflushed(committed_unit);
22678                }
22679                committed_unit = unit_members(layer, unit);
22680            }
22681            let values = match (chain_values.take(), unit) {
22682                (Some(chain), LayerUnit::Run(run)) => {
22683                    if let Err(e) = self.live_cancellation_after_work(UNIT_CANCELLED) {
22684                        self.flush_computed_write_buffer(&mut computed_writes)?;
22685                        return Err(e);
22686                    }
22687                    let members =
22688                        &layer.vertices[run.start as usize..(run.start + run.len) as usize];
22689                    let delta_active = delta.as_deref().is_some_and(|d| d.mode != DeltaMode::Off);
22690                    match self.commit_run_numbers(
22691                        run,
22692                        members,
22693                        &chain,
22694                        delta_active,
22695                        Some(&mut computed_writes),
22696                    ) {
22697                        Ok(true) => {
22698                            done += chain.len();
22699                            continue;
22700                        }
22701                        Ok(false) => {}
22702                        Err(e) => {
22703                            self.flush_computed_write_buffer(&mut computed_writes)?;
22704                            return Err(e);
22705                        }
22706                    }
22707                    members
22708                        .iter()
22709                        .copied()
22710                        .zip(chain.into_iter().map(LiteralValue::Number))
22711                        .collect()
22712                }
22713                (_, unit) => self.evaluate_unit_immutable(layer, unit),
22714            };
22715            // Post-work boundary: the unit ran while (or after) the request
22716            // was cancelled; it is not committed and stays dirty.
22717            if let Err(e) = self.live_cancellation_after_work(UNIT_CANCELLED) {
22718                self.flush_computed_write_buffer(&mut computed_writes)?;
22719                return Err(e);
22720            }
22721            done += values.len();
22722            if let LayerUnit::Run(run) = unit {
22723                let members = &layer.vertices[run.start as usize..(run.start + run.len) as usize];
22724                let delta_active = delta.as_deref().is_some_and(|d| d.mode != DeltaMode::Off);
22725                let committed = self.commit_run_scalars(
22726                    run,
22727                    members,
22728                    &values,
22729                    delta_active,
22730                    buffered.then_some(&mut computed_writes),
22731                );
22732                match committed {
22733                    Ok(true) => continue,
22734                    Ok(false) => {}
22735                    Err(e) => {
22736                        self.flush_computed_write_buffer(&mut computed_writes)?;
22737                        return Err(e);
22738                    }
22739                }
22740            }
22741            // A run unit's members were all evaluated before any commits
22742            // (FR5, FORM-192); a single cell is committed as it runs.
22743            let guarded = self.freshness_begin_batch_commit(&values);
22744            for (vertex_id, value) in values {
22745                let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
22746                let effects = if buffered {
22747                    self.plan_vertex_effects_with_computed_flush(
22748                        vertex_id,
22749                        value,
22750                        &mut computed_writes,
22751                    )
22752                } else {
22753                    self.plan_vertex_effects(vertex_id, value)
22754                };
22755                let effects = match effects {
22756                    Ok(effects) => effects,
22757                    Err(e) => {
22758                        self.flush_computed_write_buffer(&mut computed_writes)?;
22759                        return Err(e);
22760                    }
22761                };
22762                for effect in &effects {
22763                    if let Err(e) = self.apply_effect_with_computed_writes(
22764                        effect,
22765                        delta.as_deref_mut(),
22766                        log.as_deref_mut(),
22767                        buffered.then_some(&mut computed_writes),
22768                    ) {
22769                        self.flush_computed_write_buffer(&mut computed_writes)?;
22770                        return Err(e);
22771                    }
22772                }
22773                self.freshness_keep_redirtied(redirtied, vertex_id);
22774            }
22775        }
22776        self.flush_computed_write_buffer(&mut computed_writes)?;
22777        if track_unflushed {
22778            self.freshness_flushed();
22779        }
22780        // Debug builds: every chain member equals the per-cell path, now
22781        // that the members above it are written.
22782        #[cfg(debug_assertions)]
22783        if chained {
22784            for &v in &layer.vertices {
22785                let cell = self.graph.get_cell_ref(v);
22786                let (sheet, row, col) = cell
22787                    .map(|c| {
22788                        (
22789                            self.graph.sheet_name(c.sheet_id).to_string(),
22790                            c.coord.row() + 1,
22791                            c.coord.col() + 1,
22792                        )
22793                    })
22794                    .expect("chain member cell");
22795                let written = self.read_cell_value(&sheet, row, col);
22796                let oracle = self
22797                    .evaluate_vertex_immutable(v)
22798                    .unwrap_or_else(LiteralValue::Error);
22799                assert!(
22800                    written
22801                        .as_ref()
22802                        .is_some_and(|w| same_value_bits(w, &oracle)),
22803                    "chain member {sheet}!R{row}C{col}: {written:?} vs per-cell {oracle:?}"
22804                );
22805            }
22806        }
22807        #[cfg(not(debug_assertions))]
22808        let _ = chained;
22809        Ok(layer.vertices.len())
22810    }
22811
22812    /// Evaluate a layer sequentially using the effects pipeline.
22813    fn evaluate_layer_sequential_effects(
22814        &mut self,
22815        layer: &super::scheduler::Layer,
22816    ) -> Result<usize, ExcelError> {
22817        let buffered = buffer_layer_writes(layer);
22818        self.evaluate_layer_units(layer, None, None, None, buffered)
22819    }
22820
22821    /// Evaluate a layer sequentially with delta collection via effects pipeline.
22822    fn evaluate_layer_sequential_with_delta_effects(
22823        &mut self,
22824        layer: &super::scheduler::Layer,
22825        delta: &mut DeltaCollector,
22826    ) -> Result<usize, ExcelError> {
22827        let buffered = buffer_layer_writes(layer);
22828        self.evaluate_layer_units(layer, Some(delta), None, None, buffered)
22829    }
22830
22831    /// Evaluate a layer sequentially with cancellation support via effects pipeline.
22832    fn evaluate_layer_sequential_cancellable_effects(
22833        &mut self,
22834        layer: &super::scheduler::Layer,
22835        cancel_flag: &AtomicBool,
22836    ) -> Result<usize, ExcelError> {
22837        let buffered = buffer_layer_writes(layer);
22838        let cancel = (cancel_flag, 256, "Evaluation cancelled within layer");
22839        self.evaluate_layer_units(layer, None, None, Some(cancel), buffered)
22840    }
22841
22842    /// Evaluate a layer sequentially with more frequent cancellation for demand-driven eval.
22843    fn evaluate_layer_sequential_cancellable_demand_driven_effects(
22844        &mut self,
22845        layer: &super::scheduler::Layer,
22846        cancel_flag: &AtomicBool,
22847    ) -> Result<usize, ExcelError> {
22848        let buffered = buffer_layer_writes(layer);
22849        let cancel = (
22850            cancel_flag,
22851            128,
22852            "Demand-driven evaluation cancelled within layer",
22853        );
22854        self.evaluate_layer_units(layer, None, None, Some(cancel), buffered)
22855    }
22856
22857    /// Evaluate a layer in parallel, applying via effects pipeline.
22858    fn evaluate_layer_parallel_effects(
22859        &mut self,
22860        layer: &super::scheduler::Layer,
22861        min_chunk: u32,
22862    ) -> Result<usize, ExcelError> {
22863        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
22864
22865        let phases = self.parallel_phases(layer);
22866        let mut applied = 0usize;
22867
22868        for (units, group) in &phases {
22869            let group = &group[..];
22870            if group.is_empty() {
22871                continue;
22872            }
22873            let mut computed_writes = ComputedWriteBuffer::default();
22874
22875            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
22876                thread_pool.install(|| self.evaluate_units_parallel(layer, units, None, min_chunk));
22877
22878            // Post-work boundary: a group evaluated across a live
22879            // cancellation is not committed (it stays dirty).
22880            let results = results.and_then(|results| {
22881                self.live_cancellation_after_work(GROUP_CANCELLED)
22882                    .map(|()| results)
22883            });
22884            // FR3: a parallel group is one commit unit; one stale reader
22885            // drops the whole group (it stays dirty and replans).
22886            self.freshness_gate_group(group);
22887            match results {
22888                Ok(vertex_results) => {
22889                    let (vertex_results, committed) = self.commit_parallel_runs(
22890                        layer,
22891                        units,
22892                        vertex_results,
22893                        false,
22894                        &mut computed_writes,
22895                    )?;
22896                    applied = applied.saturating_add(committed);
22897                    // Arrays first, then scalars — establishes spill regions before
22898                    // scalar results that might land inside a spilled region.
22899                    // FR5 (FORM-192): members evaluated together must not
22900                    // clear a re-dirty from a spill committed before them.
22901                    let guarded = self.freshness_begin_batch_commit(&vertex_results);
22902                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
22903                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
22904                    for (vertex_id, result) in vertex_results {
22905                        if matches!(result, LiteralValue::Array(_)) {
22906                            arrays.push((vertex_id, result));
22907                        } else {
22908                            others.push((vertex_id, result));
22909                        }
22910                    }
22911                    for (vertex_id, result) in arrays {
22912                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
22913                        let effects = match self.plan_vertex_effects_with_computed_flush(
22914                            vertex_id,
22915                            result,
22916                            &mut computed_writes,
22917                        ) {
22918                            Ok(effects) => effects,
22919                            Err(e) => {
22920                                self.flush_computed_write_buffer(&mut computed_writes)?;
22921                                return Err(e);
22922                            }
22923                        };
22924                        for effect in &effects {
22925                            if let Err(e) = self.apply_effect_with_computed_writes(
22926                                effect,
22927                                None,
22928                                None,
22929                                Some(&mut computed_writes),
22930                            ) {
22931                                self.flush_computed_write_buffer(&mut computed_writes)?;
22932                                return Err(e);
22933                            }
22934                        }
22935                        self.freshness_keep_redirtied(redirtied, vertex_id);
22936                        applied = applied.saturating_add(1);
22937                    }
22938                    // Make all array spill/top-left writes visible before scalar effects in this group.
22939                    self.flush_computed_write_buffer(&mut computed_writes)?;
22940                    for (vertex_id, result) in others {
22941                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
22942                        let effects = match self.plan_vertex_effects_with_computed_flush(
22943                            vertex_id,
22944                            result,
22945                            &mut computed_writes,
22946                        ) {
22947                            Ok(effects) => effects,
22948                            Err(e) => {
22949                                self.flush_computed_write_buffer(&mut computed_writes)?;
22950                                return Err(e);
22951                            }
22952                        };
22953                        for effect in &effects {
22954                            if let Err(e) = self.apply_effect_with_computed_writes(
22955                                effect,
22956                                None,
22957                                None,
22958                                Some(&mut computed_writes),
22959                            ) {
22960                                self.flush_computed_write_buffer(&mut computed_writes)?;
22961                                return Err(e);
22962                            }
22963                        }
22964                        self.freshness_keep_redirtied(redirtied, vertex_id);
22965                        applied = applied.saturating_add(1);
22966                    }
22967                    // Flush at the group boundary; phase1 must be visible before phase2.
22968                    self.flush_computed_write_buffer(&mut computed_writes)?;
22969                }
22970                Err(e) => {
22971                    self.flush_computed_write_buffer(&mut computed_writes)?;
22972                    return Err(e);
22973                }
22974            }
22975        }
22976
22977        Ok(applied)
22978    }
22979
22980    /// Evaluate a layer in parallel with delta collection via effects pipeline.
22981    fn evaluate_layer_parallel_with_delta_effects(
22982        &mut self,
22983        layer: &super::scheduler::Layer,
22984        delta: &mut DeltaCollector,
22985    ) -> Result<usize, ExcelError> {
22986        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
22987
22988        let phases = self.parallel_phases(layer);
22989        let mut applied = 0usize;
22990
22991        for (units, group) in &phases {
22992            let group = &group[..];
22993            if group.is_empty() {
22994                continue;
22995            }
22996            let mut computed_writes = ComputedWriteBuffer::default();
22997            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
22998                thread_pool.install(|| self.evaluate_units_parallel(layer, units, None, 8));
22999
23000            // Post-work boundary: a group evaluated across a live
23001            // cancellation is not committed (it stays dirty).
23002            let results = results.and_then(|results| {
23003                self.live_cancellation_after_work(GROUP_CANCELLED)
23004                    .map(|()| results)
23005            });
23006            // FR3: a parallel group is one commit unit; one stale reader
23007            // drops the whole group (it stays dirty and replans).
23008            self.freshness_gate_group(group);
23009            match results {
23010                Ok(vertex_results) => {
23011                    let (vertex_results, committed) = self.commit_parallel_runs(
23012                        layer,
23013                        units,
23014                        vertex_results,
23015                        delta.mode != DeltaMode::Off,
23016                        &mut computed_writes,
23017                    )?;
23018                    applied = applied.saturating_add(committed);
23019                    // FR5 (FORM-192): members evaluated together must not
23020                    // clear a re-dirty from a spill committed before them.
23021                    let guarded = self.freshness_begin_batch_commit(&vertex_results);
23022                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
23023                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
23024                    for (vertex_id, result) in vertex_results {
23025                        if matches!(result, LiteralValue::Array(_)) {
23026                            arrays.push((vertex_id, result));
23027                        } else {
23028                            others.push((vertex_id, result));
23029                        }
23030                    }
23031                    for (vertex_id, result) in arrays {
23032                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
23033                        let effects = match self.plan_vertex_effects_with_computed_flush(
23034                            vertex_id,
23035                            result,
23036                            &mut computed_writes,
23037                        ) {
23038                            Ok(effects) => effects,
23039                            Err(e) => {
23040                                self.flush_computed_write_buffer(&mut computed_writes)?;
23041                                return Err(e);
23042                            }
23043                        };
23044                        for effect in &effects {
23045                            if let Err(e) = self.apply_effect_with_computed_writes(
23046                                effect,
23047                                Some(delta),
23048                                None,
23049                                Some(&mut computed_writes),
23050                            ) {
23051                                self.flush_computed_write_buffer(&mut computed_writes)?;
23052                                return Err(e);
23053                            }
23054                        }
23055                        self.freshness_keep_redirtied(redirtied, vertex_id);
23056                        applied = applied.saturating_add(1);
23057                    }
23058                    self.flush_computed_write_buffer(&mut computed_writes)?;
23059                    for (vertex_id, result) in others {
23060                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
23061                        let effects = match self.plan_vertex_effects_with_computed_flush(
23062                            vertex_id,
23063                            result,
23064                            &mut computed_writes,
23065                        ) {
23066                            Ok(effects) => effects,
23067                            Err(e) => {
23068                                self.flush_computed_write_buffer(&mut computed_writes)?;
23069                                return Err(e);
23070                            }
23071                        };
23072                        for effect in &effects {
23073                            if let Err(e) = self.apply_effect_with_computed_writes(
23074                                effect,
23075                                Some(delta),
23076                                None,
23077                                Some(&mut computed_writes),
23078                            ) {
23079                                self.flush_computed_write_buffer(&mut computed_writes)?;
23080                                return Err(e);
23081                            }
23082                        }
23083                        self.freshness_keep_redirtied(redirtied, vertex_id);
23084                        applied = applied.saturating_add(1);
23085                    }
23086                    self.flush_computed_write_buffer(&mut computed_writes)?;
23087                }
23088                Err(e) => {
23089                    self.flush_computed_write_buffer(&mut computed_writes)?;
23090                    return Err(e);
23091                }
23092            }
23093        }
23094
23095        Ok(applied)
23096    }
23097
23098    /// Evaluate a layer in parallel with cancellation support via effects pipeline.
23099    fn evaluate_layer_parallel_cancellable_effects(
23100        &mut self,
23101        layer: &super::scheduler::Layer,
23102        cancel_flag: &AtomicBool,
23103    ) -> Result<usize, ExcelError> {
23104        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
23105
23106        if cancel_flag.load(Ordering::Relaxed) {
23107            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
23108                .with_message("Parallel evaluation cancelled before starting".to_string()));
23109        }
23110
23111        let phases = self.parallel_phases(layer);
23112        let mut applied = 0usize;
23113
23114        for (units, group) in &phases {
23115            let group = &group[..];
23116            if group.is_empty() {
23117                continue;
23118            }
23119            let mut computed_writes = ComputedWriteBuffer::default();
23120
23121            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> = thread_pool
23122                .install(|| self.evaluate_units_parallel(layer, units, Some(cancel_flag), 8));
23123
23124            // Post-work boundary: a group evaluated across a live
23125            // cancellation is not committed (it stays dirty).
23126            let results = results.and_then(|results| {
23127                self.live_cancellation_after_work(GROUP_CANCELLED)
23128                    .map(|()| results)
23129            });
23130            // FR3: a parallel group is one commit unit; one stale reader
23131            // drops the whole group (it stays dirty and replans).
23132            self.freshness_gate_group(group);
23133            match results {
23134                Ok(vertex_results) => {
23135                    let (vertex_results, committed) = self.commit_parallel_runs(
23136                        layer,
23137                        units,
23138                        vertex_results,
23139                        false,
23140                        &mut computed_writes,
23141                    )?;
23142                    applied = applied.saturating_add(committed);
23143                    // FR5 (FORM-192): members evaluated together must not
23144                    // clear a re-dirty from a spill committed before them.
23145                    let guarded = self.freshness_begin_batch_commit(&vertex_results);
23146                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
23147                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
23148                    for (vertex_id, result) in vertex_results {
23149                        if matches!(result, LiteralValue::Array(_)) {
23150                            arrays.push((vertex_id, result));
23151                        } else {
23152                            others.push((vertex_id, result));
23153                        }
23154                    }
23155                    for (vertex_id, result) in arrays {
23156                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
23157                        let effects = match self.plan_vertex_effects_with_computed_flush(
23158                            vertex_id,
23159                            result,
23160                            &mut computed_writes,
23161                        ) {
23162                            Ok(effects) => effects,
23163                            Err(e) => {
23164                                self.flush_computed_write_buffer(&mut computed_writes)?;
23165                                return Err(e);
23166                            }
23167                        };
23168                        for effect in &effects {
23169                            if let Err(e) = self.apply_effect_with_computed_writes(
23170                                effect,
23171                                None,
23172                                None,
23173                                Some(&mut computed_writes),
23174                            ) {
23175                                self.flush_computed_write_buffer(&mut computed_writes)?;
23176                                return Err(e);
23177                            }
23178                        }
23179                        self.freshness_keep_redirtied(redirtied, vertex_id);
23180                        applied = applied.saturating_add(1);
23181                    }
23182                    self.flush_computed_write_buffer(&mut computed_writes)?;
23183                    for (vertex_id, result) in others {
23184                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
23185                        let effects = match self.plan_vertex_effects_with_computed_flush(
23186                            vertex_id,
23187                            result,
23188                            &mut computed_writes,
23189                        ) {
23190                            Ok(effects) => effects,
23191                            Err(e) => {
23192                                self.flush_computed_write_buffer(&mut computed_writes)?;
23193                                return Err(e);
23194                            }
23195                        };
23196                        for effect in &effects {
23197                            if let Err(e) = self.apply_effect_with_computed_writes(
23198                                effect,
23199                                None,
23200                                None,
23201                                Some(&mut computed_writes),
23202                            ) {
23203                                self.flush_computed_write_buffer(&mut computed_writes)?;
23204                                return Err(e);
23205                            }
23206                        }
23207                        self.freshness_keep_redirtied(redirtied, vertex_id);
23208                        applied = applied.saturating_add(1);
23209                    }
23210                    self.flush_computed_write_buffer(&mut computed_writes)?;
23211                }
23212                Err(e) => {
23213                    self.flush_computed_write_buffer(&mut computed_writes)?;
23214                    return Err(e);
23215                }
23216            }
23217        }
23218
23219        Ok(applied)
23220    }
23221
23222    // ── Top-level evaluate_all_logged ───────────────────────────────────────
23223
23224    /// Evaluate all dirty/volatile vertices, recording effects into a ChangeLog.
23225    ///
23226    /// This is the same flow as `evaluate_all` but threads a ChangeLog through
23227    /// every effect application so that spill commits/clears are captured.
23228    pub fn evaluate_all_logged(&mut self, log: &mut ChangeLog) -> Result<EvalResult, ExcelError> {
23229        self.ensure_not_poisoned()?;
23230        self.observe_evaluation_resource_request(EvaluationRequestKind::FullLogged, |engine| {
23231            engine.evaluate_all_logged_unobserved(log)
23232        })
23233    }
23234
23235    fn evaluate_all_logged_unobserved(
23236        &mut self,
23237        log: &mut ChangeLog,
23238    ) -> Result<EvalResult, ExcelError> {
23239        self.observe_function_semantic_epoch()?;
23240        let _source_cache = self.source_cache_session();
23241        self.validate_deterministic_mode()?;
23242        if self.config.defer_graph_building {
23243            self.build_graph_all()?;
23244        }
23245        self.require_unified_authority()?;
23246        self.begin_evaluation_request();
23247        self.reset_virtual_dep_telemetry_if_disabled();
23248        let start = crate::instant::FzInstant::now();
23249        let mut computed_vertices = 0;
23250        let mut cycle_errors = 0;
23251
23252        let mut replan_iterations = 0;
23253        const MAX_REPLAN: usize = 5;
23254        let mut telemetry = self
23255            .config
23256            .enable_virtual_dep_telemetry
23257            .then(|| self.start_virtual_dep_telemetry());
23258
23259        log.begin_compound(format!("evaluate_all(epoch={})", self.recalc_epoch));
23260
23261        let result = (|| -> Result<EvalResult, ExcelError> {
23262            loop {
23263                let to_evaluate = self.graph.get_evaluation_vertices();
23264                if to_evaluate.is_empty() {
23265                    if let Some(t) = telemetry.as_mut()
23266                        && t.bailout_reason.is_none()
23267                    {
23268                        t.bailout_reason = Some("no_work");
23269                    }
23270                    break;
23271                }
23272
23273                let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
23274                if let Some(t) = telemetry.as_mut() {
23275                    Self::accumulate_schedule_meta(t, &meta);
23276                }
23277
23278                // Walk units in condensation order: stamp cycles at their
23279                // position, evaluate layers with ChangeLog recording.
23280                self.begin_pass(&schedule);
23281                for (unit_index, &unit) in schedule.units.iter().enumerate() {
23282                    match unit {
23283                        ScheduleUnit::Cycle(i) => {
23284                            // Journal integration (design doc §4 last row): the
23285                            // ChangeLog in this path only records SpillClear /
23286                            // SpillCommit events; WriteCell effects are never
23287                            // logged (see `apply_write_cell`). Runtime SCC tasks
23288                            // write values directly and never spill (§7.9 stamps
23289                            // would-be anchors), and their spill *teardown* is the
23290                            // same unlogged `stamp_cycle_error` the Static path
23291                            // already uses here — so direct commits coexist with
23292                            // the journal cleanly, with identical semantics to
23293                            // Static. Pinned by `scc_runtime_cycles` tests.
23294                            if self.handle_cycle_unit(schedule.unit_cycle(i), None, None, None)? > 0
23295                            {
23296                                cycle_errors += 1;
23297                            }
23298                        }
23299                        ScheduleUnit::Layer(i) => {
23300                            computed_vertices +=
23301                                self.evaluate_layer_logged(schedule.unit_layer(i), log)?;
23302                        }
23303                    }
23304                    if self.stop_after_unit(&schedule, unit_index) {
23305                        break;
23306                    }
23307                }
23308
23309                let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
23310                if let Some(t) = telemetry.as_mut() {
23311                    t.changed_vdeps_total += changed_vertices.len();
23312                }
23313                self.resource_checkpoint(0)?;
23314                if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
23315                    if let Some(t) = telemetry.as_mut() {
23316                        t.bailout_reason = Some("converged");
23317                    }
23318                    break;
23319                }
23320                if replan_iterations >= MAX_REPLAN {
23321                    if let Some(mut t) = telemetry.take() {
23322                        t.bailout_reason = Some("max_replan");
23323                        t.replan_iterations = replan_iterations;
23324                        self.last_virtual_dep_telemetry = t;
23325                    }
23326                    return Err(
23327                        self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
23328                    );
23329                }
23330                replan_iterations += 1;
23331            }
23332
23333            if let Some(mut t) = telemetry {
23334                t.replan_iterations = replan_iterations;
23335                self.last_virtual_dep_telemetry = t;
23336            }
23337
23338            self.redirty_for_next_recalc();
23339            self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
23340
23341            Ok(EvalResult {
23342                computed_vertices,
23343                cycle_errors,
23344                elapsed: start.elapsed(),
23345            })
23346        })();
23347        log.end_compound();
23348        result
23349    }
23350
23351    /// Evaluate a single layer with ChangeLog recording.
23352    fn evaluate_layer_logged(
23353        &mut self,
23354        layer: &super::scheduler::Layer,
23355        log: &mut ChangeLog,
23356    ) -> Result<usize, ExcelError> {
23357        self.resource_checkpoint(layer.vertices.len() as u64)?;
23358        self.evaluate_layer_units(layer, None, Some(log), None, true)
23359    }
23360}
23361
23362impl<R: EvaluationContext> Engine<R> {
23363    /// Whether an engine-internal panic has permanently disabled work.
23364    ///
23365    /// Reads and inspection remain available, but may expose stale or partially
23366    /// installed values. Discard a poisoned engine; there is no recovery API.
23367    ///
23368    /// # Panics
23369    ///
23370    /// Infallible mutation methods linked here panic before doing work when
23371    /// poisoned, with "formualizer engine is poisoned by an earlier internal panic; discard it".
23372    /// Fallible methods return a typed EnginePoisoned error instead.
23373    pub fn is_poisoned(&self) -> bool {
23374        self.poisoned.load(std::sync::atomic::Ordering::Acquire)
23375    }
23376
23377    pub(crate) fn ensure_not_poisoned(&self) -> Result<(), ExcelError> {
23378        if self.is_poisoned() {
23379            Err(ExcelError::new(ExcelErrorKind::Value)
23380                .with_message(super::poison::POISON_MESSAGE)
23381                .with_extra(formualizer_common::ExcelErrorExtra::EnginePoisoned))
23382        } else {
23383            Ok(())
23384        }
23385    }
23386
23387    pub(crate) fn assert_not_poisoned(&self) {
23388        assert!(!self.is_poisoned(), "{}", super::poison::POISON_MESSAGE);
23389    }
23390}