Skip to main content

formualizer_eval/engine/
eval.rs

1use crate::SheetId;
2
3mod criteria;
4mod exact;
5mod family;
6mod kernels;
7mod lift;
8mod memo;
9use crate::arrow_store::{OverlayFragment, OverlayValue, SheetStore};
10#[cfg(test)]
11use crate::engine::Scheduler;
12use crate::engine::arena::AstNodeId;
13use crate::engine::eval_delta::{
14    DeltaCollector, DeltaMode, EvalDelta, EvalDeltaCompatibilityPolicy,
15};
16use crate::engine::graph::editor::change_log::MutationCapture;
17use crate::engine::graph::prepared_legacy_graph::PreparedLegacyGraphPlan;
18use crate::engine::ingest_pipeline::{DependencyPlanRow, FormulaAstInput};
19use crate::engine::live_edges::{LiveEdgeCollector, RecordingContext};
20use crate::engine::live_graph::analyze_live_graph;
21use crate::engine::lookup_index_cache::{
22    BuildOutcome, LookupAxis, LookupIndex, LookupIndexCache, LookupIndexCacheReport,
23    LookupIndexKey, estimate_bytes,
24};
25use crate::engine::named_range::{NameScope, NamedDefinition};
26use crate::engine::range_view::RangeView;
27use crate::engine::row_visibility::RowVisibilityState;
28use crate::engine::spill::{RegionLockManager, SpillMeta, SpillShape};
29use crate::engine::target_preparation::{
30    StagedFormulaIndex, StagedFormulaLease, StagedPackageLease,
31};
32use crate::engine::used_extent::{
33    ExtentPolicy, OpenRangeBounds, resolve_used_extent_with_fallback,
34};
35use crate::engine::virtual_deps::VirtualDepBuilder;
36use family::{LayerUnit, layer_units, unit_members};
37
38#[path = "freshness.rs"]
39mod freshness;
40use crate::engine::template::region::Region;
41use crate::engine::{
42    ChangeLogger, CycleDetection, CyclePolicy, DependencyGraph, EvalConfig, EvaluationRequestKind,
43    EvaluationRequestOutcome, EvaluationResourceBaselineStats, EvaluationResourceRequestStats,
44    FormulaDirtyLeaseOutcome, FormulaIngestBatch, FormulaIngestRecord, FormulaIngestReport,
45    FormulaParseDiagnostic, FormulaParsePolicy, FormulaPlaneMode, ResourceLedger,
46    RowVisibilitySource, ScheduleUnit, VertexId, VertexKind, VisibilityMaskMode,
47};
48use crate::function::FnCaps;
49use crate::interpreter::Interpreter;
50use crate::reference::{CellRef, Coord, RangeRef};
51use crate::traits::FunctionProvider;
52use crate::traits::{EvaluationContext, ReferenceInfo, Resolver};
53use formualizer_common::{
54    CoordBuildHasher, LiteralValue, col_letters_from_1based, parse_a1_1based,
55};
56use formualizer_parse::parser::ReferenceType;
57use formualizer_parse::{ASTNode, ASTNodeType, ExcelError, ExcelErrorKind};
58use rayon::ThreadPoolBuilder;
59use rustc_hash::{FxHashMap, FxHashSet};
60use std::collections::{BTreeMap, BTreeSet, VecDeque};
61use std::sync::Arc;
62use std::sync::atomic::{AtomicBool, Ordering};
63use std::time::Instant;
64
65type StagedFormulaEntry = (u32, u32, String);
66type StagedSheetParts = (
67    Vec<StagedFormulaEntry>,
68    Option<crate::engine::DeferredFormulaPackage>,
69);
70
71/// Per-sheet staged-formula store (NOTE(#126) follow-up).
72///
73/// Ingest consumers (`build_graph_all`/`build_graph_for_sheets`) walk staged
74/// entries in INSERTION order, so the order-preserving `Vec` stays the
75/// canonical storage; a `(row, col) → index` map removes the linear dup-scan
76/// that made `stage_formula_text`/`get_staged_formula_text` O(staged-on-sheet)
77/// per call (O(n²) for an n-formula deferred load on one sheet — ~570 ms for
78/// 50k stages, release, before the index). `stage`/`get` are O(1);
79/// `remove` keeps the old O(n) `Vec::remove` (rare path, order preserved).
80#[derive(Default)]
81pub(crate) struct StagedSheet {
82    entries: Vec<StagedFormulaEntry>,
83    index: FxHashMap<(u32, u32), usize>,
84    deferred_package: Option<crate::engine::DeferredFormulaPackage>,
85}
86
87impl StagedSheet {
88    fn invalidate_all_deferred_families(package: &mut crate::engine::DeferredFormulaPackage) {
89        package
90            .invalidated
91            .extend(package.families.iter().map(|family| family.source_id));
92        package.invalidated.extend(
93            package
94                .partitioned_families
95                .iter()
96                .map(|family| family.source_id),
97        );
98    }
99
100    fn invalidate_deferred_at(&mut self, row: u32, col: u32) {
101        let Some(package) = self.deferred_package.as_mut() else {
102            return;
103        };
104        if package.suppressed.contains(&(row, col)) {
105            // Previously consumed coordinates are already excluded from residual
106            // authority. Edits/deletion must not invalidate untouched fragments.
107            return;
108        }
109        if package.source_geometry_complete
110            && package.families.is_empty()
111            && package.partitioned_families.is_empty()
112        {
113            if let Some((row0, col0)) = row.checked_sub(1).zip(col.checked_sub(1))
114                && package
115                    .source_coordinates
116                    .binary_search(&crate::engine::SourceCoord {
117                        row: row0,
118                        col: col0,
119                    })
120                    .is_ok()
121            {
122                package.suppressed.insert((row, col));
123            }
124            return;
125        }
126        let family = package
127            .replay
128            .lock()
129            .ok()
130            .and_then(|mut replay| replay.formula_at(row, col).ok())
131            .flatten()
132            .and_then(|record| record.partition_owner.or(record.family));
133        if let Some(family) = family {
134            package.invalidated.insert(family);
135        } else {
136            // A poisoned lock, replay failure, malformed spool, or missing
137            // lookup result must never let a possibly edited family commit.
138            // Fail closed at package scope before suppressing the edited cell.
139            Self::invalidate_all_deferred_families(package);
140        }
141        package.suppressed.insert((row, col));
142    }
143
144    fn reconcile_attached_deferred_package(&mut self) {
145        let Some(package) = self.deferred_package.as_mut() else {
146            return;
147        };
148        if self.entries.is_empty() {
149            return;
150        }
151
152        let replayed = (|| {
153            let mut disposition = crate::engine::FormulaReplayDisposition::default();
154            for partition in &package.partitioned_families {
155                disposition.register_partition(partition, false)?;
156            }
157            package
158                .replay
159                .lock()
160                .map_err(|_| "deferred formula spool lock poisoned".to_string())?
161                .replay_partitioned(&disposition, &package.partitioned_families)
162        })();
163
164        let Ok(records) = replayed else {
165            Self::invalidate_all_deferred_families(package);
166            package
167                .suppressed
168                .extend(self.entries.iter().map(|(row, col, _)| (*row, *col)));
169            return;
170        };
171        let mut owners = FxHashMap::default();
172        for record in &records {
173            owners
174                .entry((record.row, record.col))
175                .or_insert(record.partition_owner.or(record.family));
176        }
177        for (row, col, _) in &self.entries {
178            if let Some(family) = owners.get(&(*row, *col)).copied().flatten() {
179                package.invalidated.insert(family);
180            } else {
181                Self::invalidate_all_deferred_families(package);
182            }
183            package.suppressed.insert((*row, *col));
184        }
185        package.reconciliation_replay = Some(records);
186    }
187
188    fn stage(&mut self, row: u32, col: u32, text: String) {
189        self.invalidate_deferred_at(row, col);
190        match self.index.entry((row, col)) {
191            std::collections::hash_map::Entry::Occupied(slot) => {
192                self.entries[*slot.get()].2 = text;
193            }
194            std::collections::hash_map::Entry::Vacant(slot) => {
195                slot.insert(self.entries.len());
196                self.entries.push((row, col, text));
197            }
198        }
199    }
200
201    fn remove(&mut self, row: u32, col: u32) -> Option<String> {
202        let deferred_text = self.get(row, col);
203        self.invalidate_deferred_at(row, col);
204        let Some(idx) = self.index.remove(&(row, col)) else {
205            return deferred_text;
206        };
207        let (_, _, text) = self.entries.remove(idx);
208        // `Vec::remove` shifted everything after `idx` left by one.
209        for slot in self.index.values_mut() {
210            if *slot > idx {
211                *slot -= 1;
212            }
213        }
214        Some(text)
215    }
216
217    fn get_ordinary(&self, row: u32, col: u32) -> Option<&str> {
218        self.index
219            .get(&(row, col))
220            .map(|&i| self.entries[i].2.as_str())
221    }
222
223    fn remove_ordinary(&mut self, row: u32, col: u32) -> Option<String> {
224        let idx = self.index.remove(&(row, col))?;
225        let (_, _, text) = self.entries.remove(idx);
226        for slot in self.index.values_mut() {
227            if *slot > idx {
228                *slot -= 1;
229            }
230        }
231        Some(text)
232    }
233
234    fn get(&self, row: u32, col: u32) -> Option<String> {
235        if let Some(text) = self.get_ordinary(row, col) {
236            return Some(text.to_string());
237        }
238        let package = self.deferred_package.as_ref()?;
239        if package.suppressed.contains(&(row, col)) {
240            return None;
241        }
242        package
243            .replay
244            .lock()
245            .ok()?
246            .formula_at(row, col)
247            .ok()?
248            .map(|record| record.text)
249    }
250
251    fn len(&self) -> usize {
252        self.deferred_package
253            .as_ref()
254            .map_or(self.entries.len(), |package| {
255                (if package.source_geometry_complete
256                    && (package.coordinates_cover_families
257                        || (package.families.is_empty() && package.partitioned_families.is_empty()))
258                {
259                    package.source_coordinates.len()
260                } else {
261                    usize::try_from(package.report.source_formula_records_spooled)
262                        .unwrap_or(usize::MAX)
263                })
264                .saturating_sub(package.suppressed.len())
265                .saturating_add(self.entries.len())
266            })
267    }
268
269    fn is_empty(&self) -> bool {
270        self.entries.is_empty() && self.deferred_package.is_none()
271    }
272
273    fn into_parts(self) -> StagedSheetParts {
274        (self.entries, self.deferred_package)
275    }
276}
277
278type StagedFormulaMap = std::collections::HashMap<String, StagedSheet>;
279
280#[derive(Clone, Debug, PartialEq, Eq, Hash)]
281struct PreparationRegion {
282    sheet: String,
283    sheet_id: SheetId,
284    start_row: u32,
285    start_col: u32,
286    end_row: u32,
287    end_col: u32,
288}
289
290// Target roots dedupe by identity; they keep their first-seen order.
291struct OrderedTargetProducers {
292    ordered: Vec<crate::engine::target_preparation::TargetProducer>,
293    seen: FxHashSet<crate::engine::target_preparation::TargetProducer>,
294}
295
296impl OrderedTargetProducers {
297    fn with_capacity(capacity: usize) -> Result<Self, std::collections::TryReserveError> {
298        let mut ordered = Vec::new();
299        ordered.try_reserve(capacity)?;
300        let mut seen = FxHashSet::default();
301        seen.try_reserve(capacity)?;
302        Ok(Self { ordered, seen })
303    }
304
305    fn from_ordered(
306        ordered: Vec<crate::engine::target_preparation::TargetProducer>,
307    ) -> Result<Self, std::collections::TryReserveError> {
308        let mut seen = FxHashSet::default();
309        seen.try_reserve(ordered.len())?;
310        seen.extend(ordered.iter().copied());
311        Ok(Self { ordered, seen })
312    }
313
314    fn push(
315        &mut self,
316        producer: crate::engine::target_preparation::TargetProducer,
317    ) -> Result<bool, std::collections::TryReserveError> {
318        #[cfg(test)]
319        TARGET_ROOT_DEDUP_PROBES.with(|probes| probes.set(probes.get().saturating_add(1)));
320        if self.seen.contains(&producer) {
321            return Ok(false);
322        }
323        self.seen.try_reserve(1)?;
324        self.ordered.try_reserve(1)?;
325        self.seen.insert(producer);
326        self.ordered.push(producer);
327        Ok(true)
328    }
329
330    fn len(&self) -> usize {
331        self.ordered.len()
332    }
333
334    fn into_vec(self) -> Vec<crate::engine::target_preparation::TargetProducer> {
335        self.ordered
336    }
337}
338
339#[cfg(test)]
340thread_local! {
341    static TARGET_ROOT_DEDUP_PROBES: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
342}
343
344fn target_root_allocation_error(observed: usize, request_id: Option<u64>) -> ExcelError {
345    crate::engine::ResourceLedgerError::Exhausted(formualizer_common::ResourceExhaustionDetail {
346        reason: formualizer_common::ResourceExhaustionReason::ScratchMemory,
347        limit: u64::MAX,
348        observed: observed as u64,
349        request_id,
350    })
351    .into_excel_error()
352}
353
354#[derive(Debug)]
355struct PreparedOrdinaryStagedFormula {
356    sheet: String,
357    sheet_id: SheetId,
358    lease: StagedFormulaLease,
359    ast_id: Option<AstNodeId>,
360    plan: Option<DependencyPlanRow>,
361}
362
363struct PreparedTargetSourcePackage {
364    sheet: String,
365    sheet_id: SheetId,
366    lease: StagedPackageLease,
367    selected_points: Option<BTreeSet<(u32, u32)>>,
368    complete_selections: BTreeSet<crate::engine::SourceFamilyId>,
369    deferred_shared: bool,
370    direct_domains: Vec<(
371        crate::engine::SourceFamilyId,
372        crate::engine::PlacementDomainTransport,
373    )>,
374    source_report: crate::engine::FormulaCompressedSourceReport,
375    replay_records: Vec<crate::engine::DeferredReplayFormula>,
376    spool_replays: u64,
377    disposition: crate::engine::FormulaReplayDisposition,
378    legacy: Vec<(u32, u32, AstNodeId, DependencyPlanRow)>,
379    direct_families: usize,
380    direct_cells: u64,
381    direct_fragments: u64,
382    direct_complete_families: u64,
383    direct_complete_cells: u64,
384    direct_partition_families: u64,
385    direct_partition_cells: u64,
386    anchor_parses: u64,
387    anchor_asts: u64,
388    anchor_analyses: u64,
389}
390
391impl PreparedTargetSourcePackage {
392    fn empty_selection(sheet: &str, sheet_id: SheetId, lease: StagedPackageLease) -> Self {
393        Self {
394            sheet: sheet.to_owned(),
395            sheet_id,
396            lease,
397            selected_points: Some(BTreeSet::new()),
398            complete_selections: Default::default(),
399            deferred_shared: false,
400            direct_domains: Vec::new(),
401            source_report: Default::default(),
402            replay_records: Vec::new(),
403            spool_replays: 0,
404            disposition: Default::default(),
405            legacy: Vec::new(),
406            direct_families: 0,
407            direct_cells: 0,
408            direct_fragments: 0,
409            direct_complete_families: 0,
410            direct_complete_cells: 0,
411            direct_partition_families: 0,
412            direct_partition_cells: 0,
413            anchor_parses: 0,
414            anchor_asts: 0,
415            anchor_analyses: 0,
416        }
417    }
418
419    fn direct_contains(&self, row: u32, col: u32) -> bool {
420        self.direct_domains.iter().any(|(_, domain)| {
421            let rect = domain.rect();
422            row > rect.start.row
423                && row <= rect.end.row + 1
424                && col > rect.start.col
425                && col <= rect.end.col + 1
426        })
427    }
428
429    fn fallback_records(&self) -> impl Iterator<Item = &crate::engine::DeferredReplayFormula> {
430        self.replay_records.iter().filter(|record| {
431            let Some((row, col)) = record.row.checked_sub(1).zip(record.col.checked_sub(1)) else {
432                return true;
433            };
434            let coord = crate::engine::SourceCoord { row, col };
435            let disposition = match record.family {
436                Some(family) => self.disposition.shared_disposition(family, coord),
437                None => self.disposition.ordinary_disposition(coord).0,
438            };
439            !matches!(
440                disposition,
441                crate::engine::FormulaReplayCoordinateDisposition::Direct
442                    | crate::engine::FormulaReplayCoordinateDisposition::Suppressed
443            )
444        })
445    }
446}
447
448type PreparedFormulaBatches = Vec<FormulaIngestBatch>;
449type StagedFormulaBatches = Vec<(String, StagedSheet)>;
450
451type CompressedReplayBatch = (
452    FormulaIngestBatch,
453    crate::engine::FormulaCompressedSourceBatch,
454);
455type PreparedSourceBatch = (
456    FormulaIngestBatch,
457    crate::engine::FormulaCompressedSourceReport,
458    crate::engine::FormulaCompressedPreparation,
459);
460type PreparedStagedFormulaBatches = (
461    PreparedFormulaBatches,
462    Vec<CompressedReplayBatch>,
463    Vec<PreparedSourceBatch>,
464    // Formulas whose planning can fail (see `formula_may_fail_planning`).
465    FxHashSet<crate::engine::arena::AstNodeId>,
466);
467
468/// Backend-neutral source-family ingress. Adapters may prepare candidates and
469/// submit exact replay; every formula is still materialized per cell.
470#[doc(hidden)]
471pub struct SourceFormulaIngress<'a, R> {
472    engine: &'a mut Engine<R>,
473}
474
475impl<R> SourceFormulaIngress<'_, R>
476where
477    R: EvaluationContext,
478{
479    pub fn prepare_families(
480        &mut self,
481        sheet_name: &str,
482        families: &[crate::engine::SourceFormulaFamily],
483    ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
484        self.engine.observe_function_semantic_epoch()?;
485        Ok(self
486            .engine
487            .prepare_source_formula_families(sheet_name, families))
488    }
489
490    pub fn prepare_eager_proposals(
491        &mut self,
492        sheet_name: &str,
493        families: &[crate::engine::SourceFormulaFamily],
494        partitions: &[crate::engine::PartitionedSourceFormulaFamily],
495        formula_record_count: u64,
496        replay: Box<dyn crate::engine::DeferredFormulaReplay>,
497    ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
498        self.engine.observe_function_semantic_epoch()?;
499        let replay = Arc::new(std::sync::Mutex::new(replay));
500        self.engine.prepare_source_formula_proposals(
501            sheet_name,
502            families,
503            partitions,
504            partitions,
505            formula_record_count,
506            replay,
507            &BTreeSet::new(),
508            &Default::default(),
509            None,
510        )
511    }
512
513    pub fn stage_deferred(&mut self, package: crate::engine::DeferredFormulaPackage) {
514        self.engine.stage_deferred_formula_package(package);
515    }
516
517    pub fn ingest_replay_batches(
518        &mut self,
519        batches: Vec<(
520            FormulaIngestBatch,
521            crate::engine::FormulaCompressedSourceBatch,
522        )>,
523    ) -> Result<FormulaIngestReport, ExcelError> {
524        self.engine
525            .ingest_compressed_formula_source_batches(batches)
526    }
527
528    pub fn finish_prepared(
529        &mut self,
530        batches: Vec<(
531            FormulaIngestBatch,
532            crate::engine::FormulaCompressedSourceReport,
533            crate::engine::FormulaCompressedPreparation,
534        )>,
535    ) -> Result<FormulaIngestReport, ExcelError> {
536        self.engine.finish_compressed_formula_sources(batches)
537    }
538}
539
540// Computed-write coalescing pays a fixed grouping/planning cost. For very narrow
541// layers there is not enough work to amortize it, and the direct point-write path
542// is faster while preserving the same visibility semantics.
543const COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH: usize = 8;
544
545/// Equal values, numbers by bits (debug oracles).
546#[cfg(debug_assertions)]
547fn same_value_bits(a: &LiteralValue, b: &LiteralValue) -> bool {
548    match (a, b) {
549        (LiteralValue::Number(x), LiteralValue::Number(y)) => x.to_bits() == y.to_bits(),
550        _ => a == b,
551    }
552}
553
554/// Whether a layer's computed writes are buffered: not for a chain unit,
555/// whose members read the ones written before them.
556fn buffer_layer_writes(layer: &crate::engine::scheduler::Layer) -> bool {
557    !layer.sequential && layer.vertices.len() >= COMPUTED_WRITE_COALESCING_MIN_LAYER_WIDTH
558}
559/// Adaptive layer parallelism (see `evaluate_layer_parallel`): a layer runs
560/// sequentially until either it has run `PARALLEL_LAYER_PROBE` or the rest,
561/// at the rate so far, is estimated at `PARALLEL_LAYER_WORTH` or more; then
562/// the rest goes to the thread pool. Tuned on Enron first eval (probe 30 /
563/// 100 / 300 µs, worth 50 / 150 / 400 µs; 300 / 150 best, no workbook
564/// slower).
565/// Candidate count from which schedule preparation sorts on the pool.
566const PARALLEL_SCHEDULE_MIN_CANDIDATES: usize = 16 * 1024;
567/// Plan reuse: a schedule of more candidates than this can become the
568/// base, and a request takes the base restricted to it when the base is at
569/// most `BASE_SCHEDULE_RATIO` times the request.
570const BASE_SCHEDULE_MIN_VERTICES: usize = 64;
571const BASE_SCHEDULE_RATIO: usize = 256;
572/// A restricted schedule keeps the base's layers, which can split a family
573/// run the planner would keep whole (each piece then builds its own
574/// criteria index): below this many candidates planning costs less than
575/// that (real_ops_model: 75 candidates, 31 us to plan, +0.3 ms restricted).
576const BASE_SCHEDULE_MIN_REQUEST: usize = 128;
577const PARALLEL_LAYER_PROBE: std::time::Duration = std::time::Duration::from_micros(300);
578const PARALLEL_LAYER_WORTH: std::time::Duration = std::time::Duration::from_micros(150);
579/// A member this expensive (ns, measured by the probe) is its own task.
580const EXPENSIVE_VERTEX_NS: u128 = 10_000;
581/// [`Engine::live_cancellation_after_work`] messages.
582const UNIT_CANCELLED: &str = "Evaluation cancelled; the evaluated unit was not committed";
583const GROUP_CANCELLED: &str =
584    "Parallel evaluation cancelled; the evaluated group was not committed";
585const SCC_CANCELLED: &str = "Evaluation cancelled; the evaluated cycle was not completed";
586
587#[derive(Debug, Clone, PartialEq)]
588pub(crate) enum ComputedWrite {
589    Cell {
590        seq: u64,
591        sheet_id: SheetId,
592        row0: u32,
593        col0: u32,
594        value: OverlayValue,
595        format_id: Option<crate::format::FormatId>,
596    },
597    Rect {
598        seq: u64,
599        sheet_id: SheetId,
600        sr0: u32,
601        sc0: u32,
602        values: Vec<Vec<OverlayValue>>,
603    },
604    /// Consecutive rows `row0..` of one column (a family run's commit),
605    /// each with its format.
606    Run {
607        seq: u64,
608        sheet_id: SheetId,
609        row0: u32,
610        col0: u32,
611        entries: Vec<(OverlayValue, Option<crate::format::FormatId>)>,
612    },
613}
614
615impl ComputedWrite {
616    #[inline]
617    pub(crate) fn seq(&self) -> u64 {
618        match self {
619            ComputedWrite::Cell { seq, .. }
620            | ComputedWrite::Rect { seq, .. }
621            | ComputedWrite::Run { seq, .. } => *seq,
622        }
623    }
624}
625
626#[derive(Debug, Default)]
627pub(crate) struct ComputedWriteBuffer {
628    writes: Vec<ComputedWrite>,
629    next_seq: u64,
630    estimated_bytes: usize,
631    formats_present: bool,
632}
633
634impl ComputedWriteBuffer {
635    const ENTRY_BASE_BYTES: usize = 32;
636
637    #[inline]
638    pub(crate) fn is_empty(&self) -> bool {
639        self.writes.is_empty()
640    }
641
642    #[inline]
643    pub(crate) fn len(&self) -> usize {
644        self.writes.len()
645    }
646
647    #[inline]
648    pub(crate) fn writes(&self) -> &[ComputedWrite] {
649        &self.writes
650    }
651
652    #[inline]
653    pub(crate) fn estimated_bytes(&self) -> usize {
654        self.estimated_bytes
655    }
656
657    #[cfg(feature = "tracing")]
658    fn traced_cell_count(&self) -> usize {
659        self.writes
660            .iter()
661            .map(|write| match write {
662                ComputedWrite::Cell { .. } => 1,
663                ComputedWrite::Rect { values, .. } => values.iter().map(Vec::len).sum(),
664                ComputedWrite::Run { entries, .. } => entries.len(),
665            })
666            .sum()
667    }
668
669    pub(crate) fn push_cell(
670        &mut self,
671        sheet_id: SheetId,
672        row0: u32,
673        col0: u32,
674        value: OverlayValue,
675    ) {
676        self.push_cell_with_format(sheet_id, row0, col0, value, None);
677    }
678
679    pub(crate) fn push_cell_with_format(
680        &mut self,
681        sheet_id: SheetId,
682        row0: u32,
683        col0: u32,
684        value: OverlayValue,
685        format_id: Option<crate::format::FormatId>,
686    ) {
687        let format_id = format_id.filter(|id| *id != crate::format::FormatId::GENERAL);
688        self.formats_present |= format_id.is_some();
689        let seq = self.next_sequence();
690        self.estimated_bytes = self
691            .estimated_bytes
692            .saturating_add(Self::estimate_value_bytes(&value));
693        self.writes.push(ComputedWrite::Cell {
694            seq,
695            sheet_id,
696            row0,
697            col0,
698            value,
699            format_id,
700        });
701    }
702
703    pub(crate) fn push_column_run(
704        &mut self,
705        sheet_id: SheetId,
706        row0: u32,
707        col0: u32,
708        mut entries: Vec<(OverlayValue, Option<crate::format::FormatId>)>,
709    ) {
710        let seq = self.next_sequence();
711        let mut added = 0usize;
712        for (value, format_id) in entries.iter_mut() {
713            *format_id = format_id.filter(|id| *id != crate::format::FormatId::GENERAL);
714            self.formats_present |= format_id.is_some();
715            added = added.saturating_add(Self::estimate_value_bytes(value));
716        }
717        self.estimated_bytes = self.estimated_bytes.saturating_add(added);
718        self.writes.push(ComputedWrite::Run {
719            seq,
720            sheet_id,
721            row0,
722            col0,
723            entries,
724        });
725    }
726
727    pub(crate) fn push_rect(
728        &mut self,
729        sheet_id: SheetId,
730        sr0: u32,
731        sc0: u32,
732        values: Vec<Vec<OverlayValue>>,
733    ) {
734        let seq = self.next_sequence();
735        let added = values
736            .iter()
737            .flat_map(|row| row.iter())
738            .map(Self::estimate_value_bytes)
739            .fold(0usize, usize::saturating_add);
740        self.estimated_bytes = self.estimated_bytes.saturating_add(added);
741        self.writes.push(ComputedWrite::Rect {
742            seq,
743            sheet_id,
744            sr0,
745            sc0,
746            values,
747        });
748    }
749
750    pub(crate) fn clear(&mut self) {
751        self.writes.clear();
752        self.estimated_bytes = 0;
753        self.formats_present = false;
754    }
755
756    fn take_writes(&mut self) -> (Vec<ComputedWrite>, bool) {
757        self.estimated_bytes = 0;
758        let formats_present = std::mem::take(&mut self.formats_present);
759        (std::mem::take(&mut self.writes), formats_present)
760    }
761
762    fn next_sequence(&mut self) -> u64 {
763        let seq = self.next_seq;
764        self.next_seq = self.next_seq.wrapping_add(1);
765        seq
766    }
767
768    #[inline]
769    fn estimate_value_bytes(value: &OverlayValue) -> usize {
770        Self::ENTRY_BASE_BYTES.saturating_add(value.estimated_payload_bytes())
771    }
772}
773
774#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
775struct ComputedWriteChunkKey {
776    sheet_id: SheetId,
777    col0: u32,
778    chunk_idx: usize,
779    chunk_start_row0: u32,
780}
781
782#[derive(Debug, Clone, PartialEq)]
783pub(crate) struct ComputedWriteChunkEntryPlan {
784    pub(crate) row_in_chunk: usize,
785    pub(crate) seq: u64,
786    pub(crate) value: OverlayValue,
787    pub(crate) format_id: Option<crate::format::FormatId>,
788}
789
790#[derive(Debug, Clone, PartialEq, Eq)]
791pub(crate) enum ComputedWriteChunkPlanShape {
792    Point,
793    SparseOffsets {
794        entries: usize,
795        span_len: usize,
796    },
797    DenseRange {
798        start: usize,
799        len: usize,
800    },
801    RunRange {
802        start: usize,
803        len: usize,
804        runs: usize,
805    },
806}
807
808#[derive(Debug, Clone, PartialEq, Eq)]
809pub(crate) enum ComputedWriteFormatClear {
810    Range { start: usize, end: usize },
811    Offsets(Vec<usize>),
812}
813
814#[derive(Debug, Clone, PartialEq)]
815pub(crate) enum ComputedWriteChunkFormatEffect {
816    NoFormatWork,
817    ClearStale(ComputedWriteFormatClear),
818    SetExplicit(Vec<(usize, Option<crate::format::FormatId>)>),
819}
820
821#[derive(Debug, Clone, PartialEq)]
822pub(crate) struct ComputedWriteChunkPlan {
823    pub(crate) sheet_id: SheetId,
824    pub(crate) col0: u32,
825    pub(crate) chunk_idx: usize,
826    pub(crate) chunk_start_row0: u32,
827    pub(crate) entries: Vec<ComputedWriteChunkEntryPlan>,
828    pub(crate) shape: ComputedWriteChunkPlanShape,
829    pub(crate) format_effect: ComputedWriteChunkFormatEffect,
830}
831
832#[derive(Debug, Clone, Default, PartialEq)]
833pub(crate) struct ComputedWriteCoalescingPlan {
834    pub(crate) chunks: Vec<ComputedWriteChunkPlan>,
835    pub(crate) input_cells: usize,
836    pub(crate) coalesced_cells: usize,
837    pub(crate) overwritten_cells: usize,
838}
839
840impl ComputedWriteCoalescingPlan {
841    #[inline]
842    pub(crate) fn is_empty(&self) -> bool {
843        self.chunks.is_empty()
844    }
845}
846
847impl ComputedWriteChunkPlan {
848    fn from_group(
849        key: ComputedWriteChunkKey,
850        mut entries: Vec<ComputedWriteChunkEntryPlan>,
851        formats_present: bool,
852        computed_lane_has_formats: bool,
853    ) -> (Self, usize) {
854        entries.sort_by_key(|entry| (entry.row_in_chunk, entry.seq));
855        let input_len = entries.len();
856        let mut coalesced: Vec<ComputedWriteChunkEntryPlan> = Vec::with_capacity(input_len);
857        for entry in entries {
858            if let Some(prev) = coalesced.last_mut()
859                && prev.row_in_chunk == entry.row_in_chunk
860            {
861                *prev = entry;
862                continue;
863            }
864            coalesced.push(entry);
865        }
866        let overwritten = input_len.saturating_sub(coalesced.len());
867        let shape = Self::classify_shape(&coalesced);
868        let format_effect = Self::classify_format_effect(
869            &coalesced,
870            &shape,
871            formats_present,
872            computed_lane_has_formats,
873        );
874        (
875            Self {
876                sheet_id: key.sheet_id,
877                col0: key.col0,
878                chunk_idx: key.chunk_idx,
879                chunk_start_row0: key.chunk_start_row0,
880                entries: coalesced,
881                shape,
882                format_effect,
883            },
884            overwritten,
885        )
886    }
887
888    fn classify_format_effect(
889        entries: &[ComputedWriteChunkEntryPlan],
890        shape: &ComputedWriteChunkPlanShape,
891        formats_present: bool,
892        computed_lane_has_formats: bool,
893    ) -> ComputedWriteChunkFormatEffect {
894        if !formats_present {
895            return if computed_lane_has_formats {
896                ComputedWriteChunkFormatEffect::ClearStale(Self::format_clear_spec(entries, shape))
897            } else {
898                ComputedWriteChunkFormatEffect::NoFormatWork
899            };
900        }
901
902        if entries.iter().all(|entry| entry.format_id.is_none()) {
903            return if computed_lane_has_formats {
904                ComputedWriteChunkFormatEffect::ClearStale(Self::format_clear_spec(entries, shape))
905            } else {
906                ComputedWriteChunkFormatEffect::NoFormatWork
907            };
908        }
909
910        ComputedWriteChunkFormatEffect::SetExplicit(
911            entries
912                .iter()
913                .map(|entry| (entry.row_in_chunk, entry.format_id))
914                .collect(),
915        )
916    }
917
918    fn format_clear_spec(
919        entries: &[ComputedWriteChunkEntryPlan],
920        shape: &ComputedWriteChunkPlanShape,
921    ) -> ComputedWriteFormatClear {
922        match shape {
923            ComputedWriteChunkPlanShape::DenseRange { start, len }
924            | ComputedWriteChunkPlanShape::RunRange { start, len, .. } => {
925                ComputedWriteFormatClear::Range {
926                    start: *start,
927                    end: start.saturating_add(*len),
928                }
929            }
930            ComputedWriteChunkPlanShape::Point
931            | ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
932                ComputedWriteFormatClear::Offsets(
933                    entries.iter().map(|entry| entry.row_in_chunk).collect(),
934                )
935            }
936        }
937    }
938
939    fn classify_shape(entries: &[ComputedWriteChunkEntryPlan]) -> ComputedWriteChunkPlanShape {
940        debug_assert!(!entries.is_empty());
941        if entries.len() == 1 {
942            return ComputedWriteChunkPlanShape::Point;
943        }
944
945        let start = entries[0].row_in_chunk;
946        let end = entries[entries.len() - 1].row_in_chunk;
947        let span_len = end.saturating_sub(start).saturating_add(1);
948        if span_len != entries.len() {
949            return ComputedWriteChunkPlanShape::SparseOffsets {
950                entries: entries.len(),
951                span_len,
952            };
953        }
954
955        let runs = Self::run_count(entries);
956        if runs < entries.len() {
957            ComputedWriteChunkPlanShape::RunRange {
958                start,
959                len: entries.len(),
960                runs,
961            }
962        } else {
963            ComputedWriteChunkPlanShape::DenseRange {
964                start,
965                len: entries.len(),
966            }
967        }
968    }
969
970    fn run_count(entries: &[ComputedWriteChunkEntryPlan]) -> usize {
971        let mut runs = 0usize;
972        let mut prev: Option<&OverlayValue> = None;
973        for entry in entries {
974            if prev != Some(&entry.value) {
975                runs = runs.saturating_add(1);
976                prev = Some(&entry.value);
977            }
978        }
979        runs
980    }
981}
982
983#[cfg(feature = "tracing")]
984#[derive(Default)]
985struct TraceEvaluationCounters {
986    computed_vertices: usize,
987    cycles: usize,
988}
989
990pub struct Engine<R> {
991    pub(crate) graph: DependencyGraph,
992    resolver: R,
993    pub config: EvalConfig,
994    workbook_load_limits: crate::engine::WorkbookLoadLimits,
995    /// Clock for volatile date/time builtins, wrapped in a per-recalc
996    /// snapshot: sampled once at the start of every evaluation request
997    /// ([`Self::begin_evaluation_request`]) so all `NOW()`/`TODAY()` reads in
998    /// one recalc — including SCC iteration passes — agree (spec §7.11).
999    clock: crate::timezone::SnapshotClock,
1000    thread_pool: Option<Arc<rayon::ThreadPool>>,
1001    pub recalc_epoch: u64,
1002    snapshot_id: std::sync::atomic::AtomicU64,
1003    topology_epoch: u64,
1004    cached_static_schedule: Option<CachedScheduleEntry>,
1005    /// Program 3 (plan reuse): schedules of recent earlier requests, most
1006    /// recent last (a user alternating between a few inputs recalculates
1007    /// the same few closures). Bounded by `RECENT_SCHEDULES` entries and
1008    /// `RECENT_SCHEDULE_VERTICES` candidate vertices in total.
1009    recent_schedules: Vec<CachedScheduleEntry>,
1010    /// Program 3 (plan reuse): the largest current schedule seen (usually
1011    /// the first evaluation's). A request it covers, and that is not much
1012    /// smaller, takes its schedule restricted to the request instead of
1013    /// planning (`Schedule::restrict`).
1014    base_schedule: Option<CachedScheduleEntry>,
1015    #[cfg(any(test, feature = "benchmark_internal"))]
1016    recalc_reuse_probe: std::sync::Mutex<RecalcReuseProbe>,
1017    spill_mgr: ShimSpillManager,
1018    /// Arrow-backed storage for sheet values (Phase A)
1019    arrow_sheets: SheetStore,
1020    /// Workbook-local number-format registry.
1021    format_registry: crate::format::FormatRegistry,
1022    /// Derived formula formats keyed by grid position, never graph vertex identity.
1023    derived_formats: crate::engine::derived_formats::DerivedFormats,
1024    #[cfg(test)]
1025    derived_format_operations_for_test: std::sync::atomic::AtomicU64,
1026    #[cfg(test)]
1027    family_members_for_test: std::sync::atomic::AtomicU64,
1028    #[cfg(test)]
1029    invariant_bound_members_for_test: std::sync::atomic::AtomicU64,
1030    #[cfg(test)]
1031    lifted_members_for_test: std::sync::atomic::AtomicU64,
1032    #[cfg(test)]
1033    chained_members_for_test: std::sync::atomic::AtomicU64,
1034    #[cfg(test)]
1035    lane_clean_reads_for_test: std::sync::atomic::AtomicU64,
1036    #[cfg(test)]
1037    criteria_kernel_members_for_test: std::sync::atomic::AtomicU64,
1038    #[cfg(test)]
1039    memo_hits_for_test: std::sync::atomic::AtomicU64,
1040    /// Authority build last compressed (`maybe_compress_formulas`).
1041    compressed_at_build: Option<u64>,
1042    #[cfg(test)]
1043    computed_overlay_set_explicit_entry_operations_for_test: u64,
1044    #[cfg(test)]
1045    computed_overlay_stale_clear_range_effects_for_test: u64,
1046    #[cfg(test)]
1047    computed_overlay_stale_clear_offset_attempts_for_test: u64,
1048    #[cfg(test)]
1049    computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64,
1050    /// True if any edit after bulk load; disables Arrow reads for parity
1051    has_edited: bool,
1052    /// Overlay compaction counter (Phase C instrumentation)
1053    overlay_compactions: u64,
1054
1055    // Overlay memory observability / budget (ticket 503)
1056    computed_overlay_bytes_estimate: usize,
1057    computed_overlay_mirroring_disabled: bool,
1058    /// When true, RangeView resolution materializes from graph/Arrow base per-cell.
1059    /// This preserves correctness if we stop mirroring formula/spill outputs into computed overlays.
1060    pub(crate) force_materialize_range_views: bool,
1061    // Pass-scoped cache for Arrow used-row bounds per column
1062    row_bounds_cache: std::sync::RwLock<Option<RowBoundsCache>>,
1063    // Snapshot-scoped final used-axis bounds for open-ended references.
1064    used_axis_bounds_cache: std::sync::RwLock<Option<UsedAxisBoundsCache>>,
1065    lookup_index_cache: LookupIndexCache,
1066    source_cache: Arc<std::sync::RwLock<SourceCache>>,
1067    /// Identity binding for opaque source-family preparations.
1068    source_formula_token: Arc<()>,
1069    /// Dedicated identity binding for reusable recalculation plans.
1070    recalc_plan_token: Arc<()>,
1071    /// Staged formulas by sheet when `defer_graph_building` is enabled.
1072    staged_formulas: StagedFormulaMap,
1073    /// Presence and generation authority for ordinary staged formula discovery.
1074    staged_formula_index: StagedFormulaIndex,
1075    // Occupancy invalidation only: never a formula/read dependency.
1076    blocked_pending_spills: Vec<(VertexId, CellRef, Region)>,
1077    /// Anchors whose spill is blocked by a formula or by another spill's
1078    /// cell, keyed by that blocking cell: a change to the cell (an edit, or
1079    /// the other spill clearing) wakes them. Never a read dependency.
1080    spill_blocker_waiters: SpillBlockerWaiters,
1081    /// Per-sheet row visibility sidecar state.
1082    row_visibility: FxHashMap<SheetId, RowVisibilityState>,
1083    /// Cached row visibility masks keyed by sheet/span/mode/version.
1084    row_visibility_mask_cache: std::sync::RwLock<
1085        FxHashMap<VisibilityMaskCacheKey, std::sync::Arc<arrow_array::BooleanArray>>,
1086    >,
1087    /// Non-fatal malformed formula diagnostics captured during ingest/graph-build.
1088    formula_parse_diagnostics: Vec<FormulaParseDiagnostic>,
1089    /// Last centralized formula ingest report.
1090    last_formula_ingest_report: Option<FormulaIngestReport>,
1091    /// Aggregate centralized formula ingest report for this engine.
1092    formula_ingest_report_total: FormulaIngestReport,
1093    /// Transient cancellation flag used during evaluation
1094    active_cancel_flag: Option<crate::engine::CancelToken>,
1095    /// Transient absolute deadline used by composed target and plan requests.
1096    active_evaluation_deadline: Option<Instant>,
1097
1098    /// Engine-level action depth.
1099    ///
1100    /// Ticket 614 introduces `Engine::action` as a stable, commit-only transaction surface.
1101    /// Nested actions are currently disallowed (deterministic rule) and will return an error.
1102    action_depth: u32,
1103
1104    // Phase 3b virtual-dependency convergence telemetry
1105    last_virtual_dep_telemetry: VirtualDepTelemetry,
1106    virtual_dep_fallback_activations: u64,
1107
1108    // Runtime-cycle SCC evaluation telemetry (RFC #112, Stage 2)
1109    last_cycle_telemetry: CycleTelemetry,
1110
1111    // C0 evaluation-resource observability. IDs are never reset or reused.
1112    next_evaluation_resource_request_id: u64,
1113    evaluation_resource_request_depth: usize,
1114    active_evaluation_resource_request: Option<EvaluationResourceRequestStats>,
1115    last_evaluation_resource_request: Option<EvaluationResourceRequestStats>,
1116    evaluation_resource_baseline: EvaluationResourceBaselineStats,
1117    evaluation_resource_request_started_at: Option<crate::instant::FzInstant>,
1118    evaluation_resource_budgets: crate::engine::EvaluationBudgets,
1119    evaluation_resource_config_diagnostic:
1120        Option<crate::engine::EvaluationResourceConfigDiagnostic>,
1121    active_resource_ledger: Option<ResourceLedger>,
1122    source_cache_footprints: Vec<std::sync::Weak<std::sync::atomic::AtomicU64>>,
1123    source_cache_accounted: u64,
1124
1125    /// SCC members that entered iterative calculation (`CyclePolicy::Iterate`
1126    /// with a witnessed live cycle) during the current evaluation request
1127    /// and must re-run on the next one.
1128    ///
1129    /// Excel re-evaluates circular cells on EVERY recalc (the accumulator
1130    /// contract, spec §4/§7.6), but this engine's dirty model marks SCC
1131    /// members clean after a recalc and would otherwise skip them forever.
1132    /// Resolution: members of iterating SCCs are redirtied volatile-like at
1133    /// the end of the same recalc that iterated them
1134    /// ([`Self::redirty_for_next_recalc`], called wherever
1135    /// `redirty_volatiles` runs). The set is per-recalc, never persisted:
1136    /// if an edit breaks the cycle, the next recalc's SCC task either does
1137    /// not exist or settles as phantom, nothing re-registers, and the
1138    /// redirty chain stops by itself.
1139    ///
1140    /// SCCs that landed on an exact fixed point are exempt: they go to
1141    /// [`Self::retained_scc_members`] instead and stay clean until the dirty
1142    /// graph (or a config change) reaches them (#368).
1143    pending_iterative_redirty: Vec<VertexId>,
1144    /// Opt-in snapshot request: defer only next-cycle volatile redirty.
1145    snapshot_evaluation_active: bool,
1146    snapshot_volatile_redirty_pending: bool,
1147    /// Members of iterating SCCs retained across recalcs (#368), keyed to the
1148    /// id of the retained SCC they belong to (ids come from
1149    /// `next_retained_scc_id`; grouping is only used for telemetry).
1150    ///
1151    /// An SCC is retained when the recalc that iterated it stopped because
1152    /// every member reproduced its previous value exactly (|Δ| = 0, or
1153    /// identity for non-numeric members; never NaN-converged), before the
1154    /// `max_iterations` cap, with no volatile or dynamic-reference member.
1155    /// Such an SCC is a fixed point of its own inputs: running it again with
1156    /// the same inputs cannot change any value, so it is not redirtied. The
1157    /// dirty graph remains the validity authority — any edit that reaches a
1158    /// member dirties it like any other formula and the SCC task re-runs.
1159    /// Membership is dropped when a member runs in an SCC task again (it is
1160    /// then re-retained or re-registered for per-recalc redirty), when the
1161    /// vertex is deleted, or when
1162    /// [`Self::reconcile_retained_sccs_at_request_begin`] invalidates the
1163    /// whole set because a config knob outside the graph changed.
1164    retained_scc_members: FxHashMap<VertexId, u64>,
1165    next_retained_scc_id: u64,
1166    /// [`Self::retained_scc_config_fingerprint`] as of the last retention.
1167    /// `Engine::config` is a public field, so knobs that change a retained
1168    /// SCC's result (cycle policy/tolerance, date system, determinism,
1169    /// volatile seeding) can change between recalcs without touching the
1170    /// graph; a mismatch at request begin dirties every retained member.
1171    /// Only meaningful while `retained_scc_members` is non-empty.
1172    retained_scc_config_fingerprint: u64,
1173    /// Function-registry semantic epoch and runtime-provider revision as of
1174    /// the last time retained SCCs were reconciled against them. A newer
1175    /// epoch dirties only the retained members whose formula calls a changed
1176    /// function (or every member when the change log is incomplete).
1177    retained_scc_function_epoch_seen: u64,
1178    retained_scc_provider_revision_seen: Option<u64>,
1179    /// Retained members that were already dirty when the current request
1180    /// began, with the SCC id they carried. A member still holding that id
1181    /// at the end of the request was not touched by any SCC task — its
1182    /// cycle dissolved (it evaluated as an ordinary formula) or the request
1183    /// never reached it — so [`Self::redirty_for_next_recalc`] drops it from
1184    /// the retained set instead of letting it linger.
1185    retained_scc_dirty_at_begin: Vec<(VertexId, u64)>,
1186
1187    /// Final committed values of iterating-SCC members (spec §4 persistence).
1188    /// In canonical (value-cache disabled) mode the computed overlay is the
1189    /// ONLY home of a formula's value, and structural edits clear computed
1190    /// overlays wholesale (`clear_computed_overlay_after_row/_col`) —
1191    /// destroying iteration state (accumulators reset to 0; found by the
1192    /// iterate edge corpus). This snapshot lets the next SCC task re-seed
1193    /// members whose overlay entry vanished. Members registered for
1194    /// per-recalc redirty are refreshed by
1195    /// [`Self::redirty_for_next_recalc`]; retained members are written once
1196    /// when retained. Entries are dropped when the member's SCC task ends
1197    /// without iterating or when the vertex is deleted. Empty unless
1198    /// something iterated — zero cost otherwise.
1199    iterative_state_values: FxHashMap<VertexId, LiteralValue>,
1200
1201    /// Global function-registry semantic epoch last observed.
1202    function_semantic_epoch_seen: u64,
1203    /// Runtime-provider semantic revision last observed.
1204    function_provider_revision_seen: Option<u64>,
1205
1206    #[cfg(feature = "tracing")]
1207    trace_evaluation_counters: TraceEvaluationCounters,
1208    #[cfg(test)]
1209    evaluation_request_begin_count_for_test: u64,
1210    #[cfg(any(test, feature = "test-support"))]
1211    before_prepared_span_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1212    #[cfg(test)]
1213    before_target_preparation_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1214    #[cfg(test)]
1215    before_target_planning_snapshot_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1216    #[cfg(test)]
1217    inject_target_semantic_stale_once_for_test: bool,
1218    #[cfg(test)]
1219    force_virtual_dep_changes_remaining_for_test: usize,
1220    /// Dynamic-read freshness state (design §8.2).
1221    freshness: freshness::Freshness,
1222    #[cfg(test)]
1223    fail_evaluation_commit_preflight_once_for_test: bool,
1224    #[cfg(test)]
1225    target_preparation_fault_for_test:
1226        Option<crate::engine::target_preparation::TargetPreparationFault>,
1227    #[cfg(test)]
1228    force_non_cycle_schedule_fallback_for_test: bool,
1229    #[cfg(test)]
1230    before_legacy_fallback_final_provider_sample_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1231    #[cfg(test)]
1232    after_eager_proposal_commit_hook: Option<Box<dyn FnOnce() + Send + Sync>>,
1233}
1234
1235/// This wrapper is intentionally thin for ticket 614 (commit-only): it delegates to existing
1236/// `Engine` edit methods and does not create changelog boundaries or implement rollback.
1237impl<R: EvaluationContext> Engine<R> {
1238    pub(crate) fn ingest_pipeline(&mut self) -> crate::engine::ingest_pipeline::IngestPipeline<'_> {
1239        self.graph.ingest_pipeline(&self.resolver)
1240    }
1241}
1242
1243pub struct EngineAction<'a, R>
1244where
1245    R: EvaluationContext,
1246{
1247    engine: &'a mut Engine<R>,
1248    name: String,
1249    // Complete private mutation capture used by atomic actions.
1250    // Stored as a raw pointer to avoid creating aliasing `&mut` borrows alongside `&mut Engine`.
1251    capture: Option<*mut MutationCapture>,
1252    // Optional Arrow undo journal used by `Engine::action_atomic`.
1253    // Stored as a raw pointer to avoid aliasing issues with `&mut Engine`.
1254    arrow_undo: Option<*mut crate::engine::ArrowUndoBatch>,
1255    // True when this EngineAction must enforce conservative atomic transaction policy.
1256    atomic_policy: bool,
1257}
1258
1259impl<'a, R> EngineAction<'a, R>
1260where
1261    R: EvaluationContext,
1262{
1263    #[inline]
1264    fn addr_for(&mut self, sheet: &str, row: u32, col: u32) -> crate::reference::CellRef {
1265        let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1266        let coord = crate::reference::Coord::from_excel(row, col, true, true);
1267        crate::reference::CellRef::new(sheet_id, coord)
1268    }
1269
1270    #[inline]
1271    pub fn name(&self) -> &str {
1272        &self.name
1273    }
1274
1275    #[inline]
1276    pub fn set_cell_value(
1277        &mut self,
1278        sheet: &str,
1279        row: u32,
1280        col: u32,
1281        value: LiteralValue,
1282    ) -> Result<(), crate::engine::EditorError> {
1283        if self.capture.is_some() {
1284            let old_value = self.engine.read_cell_value(sheet, row, col);
1285            let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1286            let addr = self.addr_for(sheet, row, col);
1287            let Some(capture_ptr) = self.capture else {
1288                return Err(crate::engine::EditorError::TransactionFailed {
1289                    reason: "action_with_logger: missing mutation capture".to_string(),
1290                });
1291            };
1292
1293            // For atomic journal mode, record computed overlay effects for this cell.
1294            // Delta-overlay undo is recorded semantically based on old_value/old_formula.
1295            let old_comp = if self.arrow_undo.is_some() {
1296                self.engine.read_computed_overlay_cell(sheet, row, col)
1297            } else {
1298                None
1299            };
1300
1301            if self.engine.graph_admission_enabled() {
1302                let admission =
1303                    self.engine
1304                        .graph
1305                        .preview_value_mutation(addr.sheet_id, row, col)?;
1306                self.engine.preflight_graph_admission(admission)?;
1307            }
1308            if old_formula.is_none() {
1309                old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1310            }
1311
1312            let delta_old_sem = if old_formula.is_some() {
1313                None
1314            } else {
1315                Some(old_value.clone().unwrap_or(LiteralValue::Empty))
1316            };
1317
1318            let start_len = unsafe { (&*capture_ptr).len() };
1319
1320            // Safety: `capture_ptr` comes from a unique operation-local `&mut MutationCapture`.
1321            let capture = unsafe { &mut *capture_ptr };
1322            self.engine.edit_with_capture(capture, |editor| {
1323                editor.set_cell_value_with_old_state(
1324                    addr,
1325                    value.clone(),
1326                    old_value.clone(),
1327                    old_formula.clone(),
1328                );
1329            })?;
1330            self.engine.record_structural_change(StructuralScope::Cell {
1331                sheet: addr.sheet_id,
1332                row: addr.coord.row(),
1333                col: addr.coord.col(),
1334            });
1335
1336            if let Some(undo_ptr) = self.arrow_undo {
1337                // 1) Spill snapshot operations (computed overlay rect restore).
1338                let new_events = &unsafe { (&*capture_ptr).events() }[start_len..];
1339                let undo = unsafe { &mut *undo_ptr };
1340                self.engine
1341                    .record_spill_ops_into_arrow_undo(undo, new_events);
1342
1343                // 2) Delta/computed overlay single-cell deltas.
1344                let new_comp = self.engine.read_computed_overlay_cell(sheet, row, col);
1345                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1346                let row0 = row.saturating_sub(1);
1347                let col0 = col.saturating_sub(1);
1348                let delta_new_sem = Some(value.clone());
1349                undo.record_delta_cell(sheet_id, row0, col0, delta_old_sem, delta_new_sem);
1350                undo.record_computed_cell(sheet_id, row0, col0, old_comp, new_comp);
1351            }
1352            Ok(())
1353        } else {
1354            self.engine
1355                .set_cell_value(sheet, row, col, value)
1356                .map_err(crate::engine::EditorError::from)
1357        }
1358    }
1359
1360    #[inline]
1361    pub fn set_cell_formula(
1362        &mut self,
1363        sheet: &str,
1364        row: u32,
1365        col: u32,
1366        ast: ASTNode,
1367    ) -> Result<(), crate::engine::EditorError> {
1368        if self.capture.is_some() {
1369            let old_value = self.engine.read_cell_value(sheet, row, col);
1370            let mut old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1371            let addr = self.addr_for(sheet, row, col);
1372            let Some(capture_ptr) = self.capture else {
1373                return Err(crate::engine::EditorError::TransactionFailed {
1374                    reason: "action_with_logger: missing mutation capture".to_string(),
1375                });
1376            };
1377
1378            let admitted_formula = if self.engine.graph_admission_enabled() {
1379                let placement =
1380                    CellRef::new(addr.sheet_id, Coord::from_excel(row, col, true, true));
1381                let ingested = self.engine.ingest_pipeline().ingest_formula(
1382                    FormulaAstInput::Tree(ast.clone()),
1383                    placement,
1384                    None,
1385                )?;
1386                let admission = self.engine.graph.preview_formula_mutations(&[(
1387                    addr.sheet_id,
1388                    row,
1389                    col,
1390                    ingested.dep_plan.clone(),
1391                )])?;
1392                self.engine.preflight_graph_admission(admission)?;
1393                Some((ingested.ast_id, ingested.dep_plan))
1394            } else {
1395                None
1396            };
1397            if old_formula.is_none() {
1398                old_formula = self.engine.read_cell_formula_ast(sheet, row, col);
1399            }
1400            let delta_old = if self.arrow_undo.is_some() {
1401                if old_formula.is_some() {
1402                    None
1403                } else {
1404                    Some(old_value.clone().unwrap_or(LiteralValue::Empty))
1405                }
1406            } else {
1407                None
1408            };
1409            let start_len = unsafe { (&*capture_ptr).len() };
1410
1411            // Safety: `capture_ptr` comes from a unique operation-local `&mut MutationCapture`.
1412            let capture = unsafe { &mut *capture_ptr };
1413            self.engine.edit_with_capture(capture, |editor| {
1414                if let Some((ast_id, plan)) = admitted_formula {
1415                    editor.set_cell_formula_with_prepared_plan(
1416                        addr,
1417                        ast.clone(),
1418                        old_value,
1419                        old_formula,
1420                        ast_id,
1421                        plan,
1422                    );
1423                } else {
1424                    editor.set_cell_formula_with_old_state(
1425                        addr,
1426                        ast.clone(),
1427                        old_value,
1428                        old_formula,
1429                    );
1430                }
1431            })?;
1432            self.engine.record_structural_change(StructuralScope::Cell {
1433                sheet: addr.sheet_id,
1434                row: addr.coord.row(),
1435                col: addr.coord.col(),
1436            });
1437
1438            if let Some(undo_ptr) = self.arrow_undo {
1439                let new_events = &unsafe { (&*capture_ptr).events() }[start_len..];
1440                let undo = unsafe { &mut *undo_ptr };
1441                self.engine
1442                    .record_spill_ops_into_arrow_undo(undo, new_events);
1443                let delta_new: Option<LiteralValue> = None;
1444                let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1445                let row0 = row.saturating_sub(1);
1446                let col0 = col.saturating_sub(1);
1447                undo.record_delta_cell(sheet_id, row0, col0, delta_old, delta_new);
1448            }
1449            Ok(())
1450        } else {
1451            self.engine
1452                .set_cell_formula(sheet, row, col, ast)
1453                .map_err(crate::engine::EditorError::from)
1454        }
1455    }
1456
1457    #[inline]
1458    pub fn set_row_hidden(
1459        &mut self,
1460        sheet: &str,
1461        row_1based: u32,
1462        hidden: bool,
1463        source: RowVisibilitySource,
1464    ) -> Result<(), crate::engine::EditorError> {
1465        if self.capture.is_some() {
1466            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
1467            let row0 = Engine::<R>::normalize_row_1based(row_1based)?;
1468            let old_hidden = self
1469                .engine
1470                .row_visibility
1471                .get(&sheet_id)
1472                .map(|state| state.is_row_hidden(row0, Some(source)))
1473                .unwrap_or(false);
1474            if old_hidden == hidden {
1475                return Ok(());
1476            }
1477
1478            let _ = self
1479                .engine
1480                .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
1481
1482            let Some(capture_ptr) = self.capture else {
1483                return Err(crate::engine::EditorError::TransactionFailed {
1484                    reason: "action_with_logger: missing mutation capture".to_string(),
1485                });
1486            };
1487            unsafe { &mut *capture_ptr }.record(crate::engine::ChangeEvent::SetRowVisibility {
1488                sheet_id,
1489                row0,
1490                source,
1491                old_hidden,
1492                new_hidden: hidden,
1493            });
1494
1495            Ok(())
1496        } else {
1497            self.engine
1498                .set_row_hidden(sheet, row_1based, hidden, source)
1499        }
1500    }
1501
1502    #[inline]
1503    pub fn set_rows_hidden(
1504        &mut self,
1505        sheet: &str,
1506        start_row_1based: u32,
1507        end_row_1based: u32,
1508        hidden: bool,
1509        source: RowVisibilitySource,
1510    ) -> Result<(), crate::engine::EditorError> {
1511        if self.capture.is_some() {
1512            let sheet_id = self.engine.ensure_known_sheet_id(sheet)?;
1513            let (start_row0, end_row0) =
1514                Engine::<R>::normalize_row_range_1based(start_row_1based, end_row_1based)?;
1515
1516            let Some(capture_ptr) = self.capture else {
1517                return Err(crate::engine::EditorError::TransactionFailed {
1518                    reason: "action_with_logger: missing mutation capture".to_string(),
1519                });
1520            };
1521            let capture = unsafe { &mut *capture_ptr };
1522
1523            for row0 in start_row0..=end_row0 {
1524                let old_hidden = self
1525                    .engine
1526                    .row_visibility
1527                    .get(&sheet_id)
1528                    .map(|state| state.is_row_hidden(row0, Some(source)))
1529                    .unwrap_or(false);
1530                if old_hidden == hidden {
1531                    continue;
1532                }
1533
1534                let _ = self
1535                    .engine
1536                    .set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source);
1537
1538                capture.record(crate::engine::ChangeEvent::SetRowVisibility {
1539                    sheet_id,
1540                    row0,
1541                    source,
1542                    old_hidden,
1543                    new_hidden: hidden,
1544                });
1545            }
1546
1547            Ok(())
1548        } else {
1549            self.engine
1550                .set_rows_hidden(sheet, start_row_1based, end_row_1based, hidden, source)
1551        }
1552    }
1553
1554    #[inline]
1555    pub fn insert_rows(
1556        &mut self,
1557        sheet: &str,
1558        before: u32,
1559        count: u32,
1560    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1561        if count == 0 {
1562            return Ok(crate::engine::ShiftSummary::default());
1563        }
1564        if self.capture.is_some() {
1565            let Some(capture_ptr) = self.capture else {
1566                return Err(crate::engine::EditorError::TransactionFailed {
1567                    reason: "action_atomic: missing mutation capture".to_string(),
1568                });
1569            };
1570
1571            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1572            let before0 = before.saturating_sub(1);
1573            let occupancy = self.engine.structural_row_occupancy(sheet, sheet_id);
1574            let affected_region = Engine::<R>::structural_row_region(sheet_id, before0);
1575
1576            // Graph structural insert (logged) - no snapshot bump.
1577            let summary = {
1578                let capture = unsafe { &mut *capture_ptr };
1579                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
1580                    Ok(crate::engine::ShiftSummary::default());
1581                self.engine.edit_with_capture(capture, |editor| {
1582                    editor.set_structural_occupancy(occupancy);
1583                    out = editor.insert_rows(sheet_id, before0, count);
1584                })?;
1585                out?
1586            };
1587
1588            // Arrow insert (truth) + undo op.
1589            self.engine.ensure_arrow_sheet(sheet);
1590            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
1591                asheet.insert_rows(before0 as usize, count as usize);
1592            }
1593            self.engine
1594                .purge_derived_formats_after_row(sheet_id, before0);
1595            self.engine
1596                .shift_row_visibility_insert(sheet_id, before0, count);
1597            self.engine.mark_moved_formula_vertices_dirty(&summary);
1598            self.engine
1599                .clear_computed_overlay_after_row(sheet, before0 as usize);
1600            self.engine
1601                .record_structural_change(StructuralScope::Region(affected_region));
1602            if let Some(undo_ptr) = self.arrow_undo {
1603                unsafe { &mut *undo_ptr }.record_insert_rows(sheet_id, before0, count);
1604            }
1605            Ok(summary)
1606        } else {
1607            self.engine.insert_rows(sheet, before, count)
1608        }
1609    }
1610
1611    #[inline]
1612    pub fn delete_rows(
1613        &mut self,
1614        sheet: &str,
1615        start: u32,
1616        count: u32,
1617    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1618        if count == 0 {
1619            return Ok(crate::engine::ShiftSummary::default());
1620        }
1621        if self.atomic_policy {
1622            return Err(crate::engine::EditorError::TransactionUnsupported {
1623                reason:
1624                    "delete_rows is not supported inside atomic actions (conservative rollback policy)"
1625                        .to_string(),
1626            });
1627        }
1628        self.engine.delete_rows(sheet, start, count)
1629    }
1630
1631    #[inline]
1632    pub fn insert_columns(
1633        &mut self,
1634        sheet: &str,
1635        before: u32,
1636        count: u32,
1637    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1638        if count == 0 {
1639            return Ok(crate::engine::ShiftSummary::default());
1640        }
1641        if self.capture.is_some() {
1642            let Some(capture_ptr) = self.capture else {
1643                return Err(crate::engine::EditorError::TransactionFailed {
1644                    reason: "action_atomic: missing mutation capture".to_string(),
1645                });
1646            };
1647
1648            let sheet_id = self.engine.graph.sheet_id_mut(sheet);
1649            let before0 = before.saturating_sub(1);
1650            let occupancy = self.engine.structural_column_occupancy();
1651            let affected_region = Engine::<R>::structural_col_region(sheet_id, before0);
1652
1653            let summary = {
1654                let capture = unsafe { &mut *capture_ptr };
1655                let mut out: Result<crate::engine::ShiftSummary, crate::engine::EditorError> =
1656                    Ok(crate::engine::ShiftSummary::default());
1657                self.engine.edit_with_capture(capture, |editor| {
1658                    editor.set_structural_occupancy(occupancy);
1659                    out = editor.insert_columns(sheet_id, before0, count);
1660                })?;
1661                out?
1662            };
1663
1664            self.engine.ensure_arrow_sheet(sheet);
1665            if let Some(asheet) = self.engine.arrow_sheets.sheet_mut(sheet) {
1666                asheet.insert_columns(before0 as usize, count as usize);
1667            }
1668            self.engine
1669                .purge_derived_formats_after_col(sheet_id, before0);
1670            self.engine.mark_moved_formula_vertices_dirty(&summary);
1671            self.engine
1672                .clear_computed_overlay_after_col(sheet, before0 as usize);
1673            self.engine
1674                .record_structural_change(StructuralScope::Region(affected_region));
1675            if let Some(undo_ptr) = self.arrow_undo {
1676                unsafe { &mut *undo_ptr }.record_insert_cols(sheet_id, before0, count);
1677            }
1678            Ok(summary)
1679        } else {
1680            self.engine.insert_columns(sheet, before, count)
1681        }
1682    }
1683
1684    #[inline]
1685    pub fn delete_columns(
1686        &mut self,
1687        sheet: &str,
1688        start: u32,
1689        count: u32,
1690    ) -> Result<crate::engine::ShiftSummary, crate::engine::EditorError> {
1691        if count == 0 {
1692            return Ok(crate::engine::ShiftSummary::default());
1693        }
1694        if self.atomic_policy {
1695            return Err(crate::engine::EditorError::TransactionUnsupported {
1696                reason:
1697                    "delete_columns is not supported inside atomic actions (conservative rollback policy)"
1698                        .to_string(),
1699            });
1700        }
1701        self.engine.delete_columns(sheet, start, count)
1702    }
1703
1704    /// Start an action from within an action.
1705    ///
1706    /// Nested actions are currently disallowed (ticket 614), so this will return a
1707    /// `EditorError::TransactionFailed` while an outer action is active.
1708    #[inline]
1709    pub fn action<T>(
1710        &mut self,
1711        name: impl AsRef<str>,
1712        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
1713    ) -> Result<T, crate::engine::EditorError> {
1714        self.engine.action(name, f)
1715    }
1716}
1717
1718struct ActionDepthGuard<'a, R> {
1719    engine: *mut Engine<R>,
1720    _marker: std::marker::PhantomData<&'a mut Engine<R>>,
1721}
1722
1723impl<'a, R> Drop for ActionDepthGuard<'a, R> {
1724    fn drop(&mut self) {
1725        // Safety: the guard is created from a unique `&mut Engine` borrow and lives no longer
1726        // than the surrounding `Engine::action` call.
1727        unsafe {
1728            let e = &mut *self.engine;
1729            e.action_depth = e.action_depth.saturating_sub(1);
1730        }
1731    }
1732}
1733
1734#[derive(Default)]
1735struct SourceCache {
1736    scalars: FxHashMap<(String, Option<u64>), LiteralValue>,
1737    tables: FxHashMap<(String, Option<u64>), Arc<dyn crate::traits::Table>>,
1738}
1739
1740#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1741struct VisibilityMaskCacheKey {
1742    sheet_id: SheetId,
1743    start_row0: u32,
1744    end_row0: u32,
1745    mode: VisibilityMaskMode,
1746    version: u64,
1747}
1748
1749#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1750enum StructuralScope {
1751    Cell { sheet: SheetId, row: u32, col: u32 },
1752    Region(Region),
1753    Sheet(SheetId),
1754    RemovedSheet(SheetId),
1755    OpaqueGlobal,
1756    AllSheets,
1757}
1758
1759/// Anchors blocked by a formula or another spill, indexed by the blocking
1760/// cell so that a single-cell change finds its waiters in O(1).
1761#[derive(Default)]
1762struct SpillBlockerWaiters {
1763    by_cell: FxHashMap<CellRef, Vec<VertexId>>,
1764    by_anchor: FxHashMap<VertexId, CellRef>,
1765}
1766
1767impl SpillBlockerWaiters {
1768    /// Largest region scanned cell by cell; a larger one scans the waiters.
1769    const REGION_PROBE_CELLS: u64 = 4096;
1770
1771    fn is_empty(&self) -> bool {
1772        self.by_anchor.is_empty()
1773    }
1774
1775    fn wait(&mut self, anchor: VertexId, blocker: CellRef) {
1776        match self.by_anchor.insert(anchor, blocker) {
1777            Some(old) if old == blocker => return,
1778            Some(old) => self.unlink(anchor, old),
1779            None => {}
1780        }
1781        self.by_cell.entry(blocker).or_default().push(anchor);
1782    }
1783
1784    fn forget(&mut self, anchor: VertexId) {
1785        if self.by_anchor.is_empty() {
1786            return;
1787        }
1788        if let Some(old) = self.by_anchor.remove(&anchor) {
1789            self.unlink(anchor, old);
1790        }
1791    }
1792
1793    fn unlink(&mut self, anchor: VertexId, cell: CellRef) {
1794        if let Some(waiters) = self.by_cell.get_mut(&cell) {
1795            waiters.retain(|&v| v != anchor);
1796            if waiters.is_empty() {
1797                self.by_cell.remove(&cell);
1798            }
1799        }
1800    }
1801
1802    /// Remove and return the anchors waiting on a cell inside `scope`.
1803    fn take_affected(&mut self, scope: StructuralScope) -> Vec<VertexId> {
1804        let cells: Vec<CellRef> = match scope {
1805            StructuralScope::Cell { sheet, row, col } => {
1806                let cell = CellRef::new(sheet, Coord::new(row, col, true, true));
1807                if self.by_cell.contains_key(&cell) {
1808                    vec![cell]
1809                } else {
1810                    Vec::new()
1811                }
1812            }
1813            StructuralScope::Region(region) => {
1814                let (rows, cols) = region.axis_ranges();
1815                let (r0, r1) = rows.query_bounds();
1816                let (c0, c1) = cols.query_bounds();
1817                let area = u64::from(r1.saturating_sub(r0) + 1)
1818                    .saturating_mul(u64::from(c1.saturating_sub(c0) + 1));
1819                if area <= Self::REGION_PROBE_CELLS {
1820                    let sheet = region.sheet_id();
1821                    (r0..=r1)
1822                        .flat_map(|r| (c0..=c1).map(move |c| (r, c)))
1823                        .map(|(r, c)| CellRef::new(sheet, Coord::new(r, c, true, true)))
1824                        .filter(|cell| self.by_cell.contains_key(cell))
1825                        .collect()
1826                } else {
1827                    self.by_cell
1828                        .keys()
1829                        .filter(|cell| {
1830                            region.intersects(&Region::point(
1831                                cell.sheet_id,
1832                                cell.coord.row(),
1833                                cell.coord.col(),
1834                            ))
1835                        })
1836                        .copied()
1837                        .collect()
1838                }
1839            }
1840            StructuralScope::Sheet(sheet) | StructuralScope::RemovedSheet(sheet) => self
1841                .by_cell
1842                .keys()
1843                .filter(|cell| cell.sheet_id == sheet)
1844                .copied()
1845                .collect(),
1846            StructuralScope::OpaqueGlobal | StructuralScope::AllSheets => {
1847                self.by_cell.keys().copied().collect()
1848            }
1849        };
1850        let mut woken = Vec::new();
1851        for cell in cells {
1852            if let Some(waiters) = self.by_cell.remove(&cell) {
1853                for anchor in waiters {
1854                    self.by_anchor.remove(&anchor);
1855                    woken.push(anchor);
1856                }
1857            }
1858        }
1859        woken
1860    }
1861}
1862
1863#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1864enum LoggedEditImpact {
1865    NoOp,
1866    DataOnly,
1867    Topology,
1868}
1869
1870#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1871enum LoggedEditDirection {
1872    Original,
1873    InverseReplay,
1874    ForwardReplay,
1875}
1876
1877#[derive(Clone, Copy)]
1878struct InvalidationBaseline {
1879    snapshot_id: u64,
1880    topology_epoch: u64,
1881}
1882
1883struct SourceCacheSession {
1884    cache: Arc<std::sync::RwLock<SourceCache>>,
1885}
1886
1887impl Drop for SourceCacheSession {
1888    fn drop(&mut self) {
1889        if let Ok(mut g) = self.cache.write() {
1890            *g = SourceCache::default();
1891        }
1892    }
1893}
1894
1895#[derive(Debug)]
1896#[non_exhaustive]
1897pub struct EvalResult {
1898    pub computed_vertices: usize,
1899    pub cycle_errors: usize,
1900    pub elapsed: std::time::Duration,
1901}
1902
1903#[derive(Clone, Debug, PartialEq, Eq)]
1904#[non_exhaustive]
1905pub struct TableMetadata {
1906    pub name: String,
1907    pub sheet: String,
1908    pub start_row: u32,
1909    pub start_col: u32,
1910    pub end_row: u32,
1911    pub end_col: u32,
1912    pub header_row: bool,
1913    pub headers: Vec<String>,
1914    pub totals_row: bool,
1915}
1916
1917/// Read-only engine counters used by benchmark/instrumentation tooling.
1918///
1919/// These counters are deliberately observational: collecting them must not mutate engine state or
1920/// alter formula evaluation semantics.
1921///
1922/// The `formula_plane_*` counters are always `0`: FormulaPlane spans were removed and the
1923/// dependency authority is the only runtime path. They are kept for source compatibility.
1924#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1925#[non_exhaustive]
1926pub struct EngineBaselineStats {
1927    pub graph_vertex_count: usize,
1928    pub graph_formula_vertex_count: usize,
1929    pub graph_edge_count: usize,
1930    pub dirty_vertex_count: usize,
1931    pub evaluation_vertex_count: usize,
1932    pub formula_ast_root_count: usize,
1933    pub formula_ast_node_count: usize,
1934    pub staged_formula_count: usize,
1935    pub formula_plane_active_span_count: usize,
1936    pub formula_plane_producer_result_entries: usize,
1937    pub formula_plane_consumer_read_entries: usize,
1938    pub formula_plane_mixed_topology_cache_builds: u64,
1939    pub formula_plane_mixed_topology_cache_hits: u64,
1940    pub formula_plane_mixed_topology_cache_overflows: u64,
1941    pub formula_plane_dirty_pending_events: usize,
1942    pub formula_plane_dirty_region_events_recorded: u64,
1943    pub formula_plane_dirty_span_region_events_recorded: u64,
1944    pub formula_plane_dirty_whole_span_seeds_recorded: u64,
1945    pub formula_plane_dirty_global_invalidations: u64,
1946    pub formula_plane_structural_span_candidates: u64,
1947    pub formula_plane_cycle_member_span_demotions: u64,
1948    pub formula_plane_array_result_span_demotions: u64,
1949    /// Members of exactly converged iterative SCCs currently retained across
1950    /// recalcs (#368).
1951    pub retained_scc_members: usize,
1952}
1953
1954#[derive(Debug, Clone, Default)]
1955#[non_exhaustive]
1956pub struct VirtualDepTelemetry {
1957    pub candidate_vertices_total: usize,
1958    pub vdeps_vertices_total: usize,
1959    pub vdeps_edges_total: usize,
1960    pub builder_elapsed_ms_total: u128,
1961    pub schedule_virtual_passes: usize,
1962    pub schedule_static_passes: usize,
1963    pub schedule_cache_hits: usize,
1964    pub schedule_cache_misses: usize,
1965    pub reused_schedule_vertices_total: usize,
1966    pub replan_iterations: usize,
1967    pub changed_vdeps_total: usize,
1968    pub bailout_reason: Option<&'static str>,
1969    pub fallback_mode_activations: u64,
1970}
1971
1972/// Per-recalc telemetry for SCC evaluation under `CycleDetection::Runtime`
1973/// (spec `formualizer-cycle-semantics-spec.md` §10).
1974///
1975/// Collection is unconditional: SCC tasks are rare relative to ordinary
1976/// vertex evaluation and the counters are a handful of integer adds per
1977/// task, so no config flag gates them (unlike [`VirtualDepTelemetry`],
1978/// which pays per-schedule costs). Counters reset at the start of every
1979/// evaluation request.
1980#[derive(Debug, Clone, Default, PartialEq)]
1981#[non_exhaustive]
1982pub struct CycleTelemetry {
1983    /// SCC tasks executed (static SCCs that reached Runtime evaluation).
1984    pub static_sccs: usize,
1985    /// SCC tasks whose live subgraph was acyclic — values produced.
1986    pub phantom_sccs: usize,
1987    /// Distinct live cycles witnessed across all SCC tasks.
1988    pub live_cycles_witnessed: usize,
1989    /// Cells stamped `#CIRC!` by Runtime SCC tasks.
1990    pub circ_cells_stamped: usize,
1991    /// Evaluation sweeps over (subsets of) SCC members, totalled across tasks
1992    /// (pass 1 included).
1993    pub settle_passes_total: usize,
1994    /// Largest pass count any single SCC task needed.
1995    pub max_passes_single_scc: usize,
1996    /// SCC tasks that entered iterative calculation (`CyclePolicy::Iterate`
1997    /// with a witnessed live cycle). RFC #113, Stage 3.
1998    pub iterated_sccs: usize,
1999    /// Iterating SCC tasks that stopped because every member passed the
2000    /// spec-§6 convergence test.
2001    pub converged_sccs: usize,
2002    /// SCC tasks that stopped at a pass cap. Under `CyclePolicy::Iterate`
2003    /// this is the Excel `max_iterations` cap (NOT an error — last values
2004    /// are kept; includes the no-convergence-test `max_iterations: 1`
2005    /// contract). Under `CyclePolicy::Error` it is the defensive acyclic
2006    /// settle cap (|SCC| + 2), which only a bug can hit.
2007    pub capped_sccs: usize,
2008    /// Largest `|Δ|` observed in any member's final-pass convergence
2009    /// comparison across iterating SCC tasks (numeric-class members only).
2010    /// `0.0` when no comparison ran (e.g. `max_iterations: 1`).
2011    pub max_abs_delta_at_stop: f64,
2012    /// Identical-bit NaN vs NaN member comparisons that were treated as
2013    /// converged (spec §6 NaN rule).
2014    pub nan_converged: usize,
2015    /// Retained iterative SCCs (#368) that had no dirty member at request begin and were therefore not
2016    /// re-run: their last exact fixed point is served as-is. Counted at
2017    /// request begin, so a demand-driven request that never reaches a
2018    /// retained SCC still reports it as reused.
2019    pub reused_sccs: usize,
2020    /// Members of the SCCs counted in `reused_sccs`.
2021    pub reused_scc_members: usize,
2022    /// Total wall-clock time spent inside Runtime SCC tasks.
2023    pub elapsed_ms: u128,
2024}
2025
2026#[derive(Debug, Clone, Copy)]
2027struct ScheduleBuildMeta {
2028    candidate_vertices: usize,
2029    vdeps_vertices: usize,
2030    vdeps_edges: usize,
2031    builder_elapsed_ms: u128,
2032    used_virtual_schedule: bool,
2033    schedule_cache_hit: bool,
2034    schedule_cache_eligible: bool,
2035}
2036
2037#[cfg(any(test, feature = "benchmark_internal"))]
2038#[doc(hidden)]
2039#[derive(Debug, Clone, Default)]
2040pub struct RecalcReuseProbe {
2041    pub schedule_requests: usize,
2042    pub schedule_cache_hits: usize,
2043    pub schedule_cache_misses: usize,
2044    pub schedule_cache_ineligible: usize,
2045    pub schedule_builds: usize,
2046    /// Program 3 plan reuse: misses served by restricting the base schedule.
2047    pub schedule_base_restrictions: usize,
2048    pub schedule_shared_handles: usize,
2049    pub schedule_retained_bytes: usize,
2050    pub legacy_target_requests: usize,
2051    pub target_schedule_builds: usize,
2052    pub demand_builds: usize,
2053    pub demand_vertices: usize,
2054    pub demand_clean_formulas: usize,
2055    pub demand_explicit_edges: usize,
2056    pub demand_virtual_builder_calls: usize,
2057}
2058
2059#[cfg(any(test, feature = "benchmark_internal"))]
2060fn schedule_probe_retained_bytes(schedule: &crate::engine::Schedule) -> usize {
2061    fn vector_bytes<T>(values: &Vec<T>) -> usize {
2062        values.capacity() * std::mem::size_of::<T>()
2063    }
2064
2065    [
2066        vector_bytes(&schedule.units),
2067        vector_bytes(&schedule.layers),
2068        vector_bytes(&schedule.cycles),
2069    ]
2070    .into_iter()
2071    .chain(
2072        schedule
2073            .layers
2074            .iter()
2075            .map(|layer| vector_bytes(&layer.vertices)),
2076    )
2077    .chain(schedule.cycles.iter().map(vector_bytes))
2078    .sum()
2079}
2080
2081#[derive(Debug, Clone)]
2082struct CachedScheduleEntry {
2083    topology_epoch: u64,
2084    /// Authority `(store revision, rev.dyn)` the schedule was planned from
2085    /// (design §8.4; always 0 without `unified_authority`).
2086    authority_revision: (u64, u64),
2087    /// The request's vertex list as runs of consecutive ids (formula ids
2088    /// come in column runs, so a whole-workbook request is a few runs).
2089    candidate_vertices: VertexIdRuns,
2090    schedule: Arc<crate::engine::scheduler::Schedule>,
2091}
2092
2093/// A vertex list stored as `(first id, run length)` runs of consecutive
2094/// ids, in list order.
2095#[derive(Debug, Clone, Default)]
2096struct VertexIdRuns(Vec<(u32, u32)>);
2097
2098impl VertexIdRuns {
2099    fn from_slice(ids: &[VertexId]) -> Self {
2100        let mut runs: Vec<(u32, u32)> = Vec::new();
2101        for v in ids {
2102            match runs.last_mut() {
2103                Some((first, len)) if first.checked_add(*len) == Some(v.0) => *len += 1,
2104                _ => runs.push((v.0, 1)),
2105            }
2106        }
2107        runs.shrink_to_fit();
2108        Self(runs)
2109    }
2110
2111    fn equals(&self, ids: &[VertexId]) -> bool {
2112        let mut rest = ids;
2113        for &(first, len) in &self.0 {
2114            let len = len as usize;
2115            if rest.len() < len {
2116                return false;
2117            }
2118            let (head, tail) = rest.split_at(len);
2119            if head
2120                .iter()
2121                .enumerate()
2122                .any(|(i, v)| v.0 != first.wrapping_add(i as u32))
2123            {
2124                return false;
2125            }
2126            rest = tail;
2127        }
2128        rest.is_empty()
2129    }
2130
2131    fn heap_bytes(&self) -> usize {
2132        self.0.capacity() * std::mem::size_of::<(u32, u32)>()
2133    }
2134
2135    /// The number of ids.
2136    fn len(&self) -> usize {
2137        self.0.iter().map(|&(_, len)| len as usize).sum()
2138    }
2139}
2140
2141#[cfg(test)]
2142mod vertex_id_runs_tests {
2143    use super::{VertexId, VertexIdRuns};
2144
2145    #[test]
2146    fn runs_compare_like_the_list() {
2147        let ids = |v: &[u32]| v.iter().map(|&i| VertexId(i)).collect::<Vec<_>>();
2148        let list = ids(&[5, 6, 7, 2, 3, 9, 10, 10]);
2149        let runs = VertexIdRuns::from_slice(&list);
2150        assert_eq!(runs.0, vec![(5, 3), (2, 2), (9, 2), (10, 1)]);
2151        assert!(runs.equals(&list));
2152        assert!(!runs.equals(&list[..7]));
2153        assert!(!runs.equals(&ids(&[5, 6, 7, 2, 3, 9, 10, 11])));
2154        assert!(!runs.equals(&ids(&[5, 6, 7, 2, 3, 9, 10, 10, 11])));
2155        assert!(VertexIdRuns::from_slice(&[]).equals(&[]));
2156        assert!(!VertexIdRuns::from_slice(&[]).equals(&list));
2157        let edge = ids(&[u32::MAX - 1, u32::MAX, 0]);
2158        assert!(VertexIdRuns::from_slice(&edge).equals(&edge));
2159    }
2160}
2161
2162/// Uncacheable requests keep their schedule inline without a shared allocation.
2163enum EvaluationSchedule {
2164    Owned(crate::engine::scheduler::Schedule),
2165    Shared(Arc<crate::engine::scheduler::Schedule>),
2166}
2167
2168impl std::ops::Deref for EvaluationSchedule {
2169    type Target = crate::engine::scheduler::Schedule;
2170
2171    fn deref(&self) -> &Self::Target {
2172        match self {
2173            Self::Owned(schedule) => schedule,
2174            Self::Shared(schedule) => schedule,
2175        }
2176    }
2177}
2178
2179type ScheduleBuildOutput = (
2180    crate::engine::scheduler::Schedule,
2181    FxHashMap<VertexId, Vec<VertexId>>,
2182    ScheduleBuildMeta,
2183);
2184
2185type EvaluationScheduleBuildOutput = (
2186    EvaluationSchedule,
2187    FxHashMap<VertexId, Vec<VertexId>>,
2188    ScheduleBuildMeta,
2189);
2190
2191/// Opaque, revision-bound recalculation recipe.
2192#[derive(Debug)]
2193pub struct RecalcPlan {
2194    key: RecalcPlanKey,
2195    kind: RecalcPlanKind,
2196}
2197
2198#[derive(Debug)]
2199struct RecalcPlanKey {
2200    engine_token: Arc<()>,
2201    revisions: PlanningRevisionSnapshot,
2202}
2203
2204#[derive(Clone, Debug, PartialEq, Eq)]
2205struct PlanningRevisionSnapshot {
2206    engine_topology_epoch: u64,
2207    graph_topology_revision: u64,
2208    staged: u64,
2209    symbols: u64,
2210    semantic: u64,
2211    provider: Option<u64>,
2212    deterministic_mode: crate::engine::DeterministicMode,
2213    budgets: crate::engine::EvaluationBudgets,
2214}
2215
2216#[derive(Debug)]
2217enum RecalcPlanKind {
2218    CompatibilityFull {
2219        schedule: crate::engine::Schedule,
2220        has_dynamic_refs: bool,
2221    },
2222    Target {
2223        targets: Vec<crate::engine::EvaluationTarget>,
2224        scope: crate::engine::PrepareScope,
2225        topology: RecalcTopology,
2226        dynamic_policy: DynamicPlanPolicy,
2227    },
2228}
2229
2230#[derive(Debug)]
2231enum RecalcTopology {
2232    RunLocalRecipe,
2233    Workbook,
2234}
2235
2236#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2237enum DynamicPlanPolicy {
2238    BoundedTargetReplan,
2239}
2240
2241impl RecalcPlan {
2242    /// Returns the retained compatibility schedule depth. Target plans retain a
2243    /// run-local recipe rather than a schedule, so their layer count is zero.
2244    pub fn layer_count(&self) -> usize {
2245        match &self.kind {
2246            RecalcPlanKind::CompatibilityFull { schedule, .. } => schedule.layers.len(),
2247            RecalcPlanKind::Target { .. } => 0,
2248        }
2249    }
2250
2251    pub fn has_dynamic_refs(&self) -> bool {
2252        match &self.kind {
2253            RecalcPlanKind::CompatibilityFull {
2254                has_dynamic_refs, ..
2255            } => *has_dynamic_refs,
2256            RecalcPlanKind::Target { .. } => false,
2257        }
2258    }
2259
2260    #[cfg(test)]
2261    pub(crate) fn force_stale_reasons_for_test(
2262        &mut self,
2263        reasons: &[formualizer_common::PlanStaleReason],
2264    ) {
2265        use formualizer_common::PlanStaleReason;
2266        for reason in reasons {
2267            match reason {
2268                PlanStaleReason::Engine => {
2269                    self.key.engine_token = Arc::new(());
2270                }
2271                PlanStaleReason::Provider => {
2272                    self.key.revisions.provider = Some(
2273                        self.key
2274                            .revisions
2275                            .provider
2276                            .unwrap_or_default()
2277                            .wrapping_add(1),
2278                    );
2279                }
2280                PlanStaleReason::Semantic => {
2281                    self.key.revisions.semantic = self.key.revisions.semantic.wrapping_add(1);
2282                }
2283                PlanStaleReason::Budget => {
2284                    let current = self.key.revisions.budgets.work.max_work_units;
2285                    self.key.revisions.budgets.work.max_work_units =
2286                        Some(current.unwrap_or_default().wrapping_add(1));
2287                }
2288                PlanStaleReason::Staged => {
2289                    self.key.revisions.staged = self.key.revisions.staged.wrapping_add(1);
2290                }
2291                PlanStaleReason::Symbols => {
2292                    self.key.revisions.symbols = self.key.revisions.symbols.wrapping_add(1);
2293                }
2294                PlanStaleReason::Graph => {
2295                    self.key.revisions.graph_topology_revision =
2296                        self.key.revisions.graph_topology_revision.wrapping_add(1);
2297                }
2298                _ => {}
2299            }
2300        }
2301    }
2302}
2303
2304#[cfg(any(test, feature = "test-support"))]
2305pub(crate) mod criteria_mask_test_hooks {
2306    use std::cell::Cell;
2307
2308    thread_local! {
2309        static MASK_CALLS_ROWS: Cell<(usize, usize)> = const { Cell::new((0, 0)) };
2310        static TEXT_SEGMENTS_TOTAL: Cell<usize> = const { Cell::new(0) };
2311        static TEXT_SEGMENTS_ALL_NULL: Cell<usize> = const { Cell::new(0) };
2312    }
2313
2314    pub(crate) fn take_mask_work() -> (usize, usize) {
2315        MASK_CALLS_ROWS.with(|c| c.replace((0, 0)))
2316    }
2317
2318    pub(crate) fn note_mask(rows: usize) {
2319        MASK_CALLS_ROWS.with(|c| {
2320            let (calls, work) = c.get();
2321            c.set((calls + 1, work + rows));
2322        });
2323    }
2324
2325    pub fn reset_text_segment_counters() {
2326        TEXT_SEGMENTS_TOTAL.with(|c| c.set(0));
2327        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(0));
2328    }
2329
2330    pub fn text_segment_counters() -> (usize, usize) {
2331        let a = TEXT_SEGMENTS_TOTAL.with(|c| c.get());
2332        let b = TEXT_SEGMENTS_ALL_NULL.with(|c| c.get());
2333        (a, b)
2334    }
2335
2336    pub(crate) fn inc_total() {
2337        TEXT_SEGMENTS_TOTAL.with(|c| c.set(c.get() + 1));
2338    }
2339    pub(crate) fn inc_all_null() {
2340        TEXT_SEGMENTS_ALL_NULL.with(|c| c.set(c.get() + 1));
2341    }
2342}
2343
2344#[cfg(test)]
2345pub(crate) mod visibility_mask_test_hooks {
2346    use std::cell::Cell;
2347
2348    thread_local! {
2349        static HITS: Cell<usize> = const { Cell::new(0) };
2350        static MISSES: Cell<usize> = const { Cell::new(0) };
2351        static EVICTIONS: Cell<usize> = const { Cell::new(0) };
2352    }
2353
2354    pub fn reset() {
2355        HITS.with(|c| c.set(0));
2356        MISSES.with(|c| c.set(0));
2357        EVICTIONS.with(|c| c.set(0));
2358    }
2359
2360    pub fn counters() -> (usize, usize, usize) {
2361        let hits = HITS.with(|c| c.get());
2362        let misses = MISSES.with(|c| c.get());
2363        let evictions = EVICTIONS.with(|c| c.get());
2364        (hits, misses, evictions)
2365    }
2366
2367    pub(crate) fn inc_hit() {
2368        HITS.with(|c| c.set(c.get() + 1));
2369    }
2370
2371    pub(crate) fn inc_miss() {
2372        MISSES.with(|c| c.set(c.get() + 1));
2373    }
2374
2375    pub(crate) fn inc_eviction() {
2376        EVICTIONS.with(|c| c.set(c.get() + 1));
2377    }
2378}
2379
2380fn is_numeric_text_equality(pred: &crate::args::CriteriaPredicate) -> bool {
2381    match pred {
2382        crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(text)) => {
2383            text.trim().parse::<f64>().is_ok_and(f64::is_finite)
2384        }
2385        _ => false,
2386    }
2387}
2388
2389/// Resolve the null (non-number) rows of an `=n` / `<>n` criteria mask over
2390/// one row segment: numeric text compares by value, every other non-number
2391/// cell (blank, boolean, error) is unequal. Segments without text stay
2392/// vectorized.
2393fn fill_numeric_equality_nulls(
2394    view: &RangeView<'_>,
2395    row_start: usize,
2396    row_len: usize,
2397    col_in_view: usize,
2398    mask: &arrow_array::BooleanArray,
2399    n: f64,
2400    ne: bool,
2401) -> Option<arrow_array::BooleanArray> {
2402    use arrow_array::Array as _;
2403    let texts = view.slice_lowered_text(row_start, row_len);
2404    let text = texts.get(col_in_view).and_then(|t| t.as_ref());
2405    match text {
2406        None => {
2407            if !ne {
2408                // Nulls are already "no match".
2409                return Some(mask.clone());
2410            }
2411            let nulls = arrow::compute::is_null(mask).ok()?;
2412            crate::compute_prelude::boolean::or_kleene(mask, &nulls).ok()
2413        }
2414        Some(text) => {
2415            if text.len() != mask.len() {
2416                return None;
2417            }
2418            let mut out = arrow_array::builder::BooleanBuilder::with_capacity(mask.len());
2419            for i in 0..mask.len() {
2420                if mask.is_valid(i) {
2421                    out.append_value(mask.value(i));
2422                } else if text.is_valid(i) {
2423                    let equal = crate::locale::Locale::invariant()
2424                        .parse_number_invariant(text.value(i))
2425                        .is_some_and(|x| (x - n).abs() < 1e-12);
2426                    out.append_value(equal != ne);
2427                } else {
2428                    out.append_value(ne);
2429                }
2430            }
2431            Some(out.finish())
2432        }
2433    }
2434}
2435
2436fn compute_criteria_mask(
2437    view: &RangeView<'_>,
2438    col_in_view: usize,
2439    pred: &crate::args::CriteriaPredicate,
2440) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
2441    use crate::compute_prelude::{boolean, cmp, concat_arrays};
2442    use arrow::compute::kernels::comparison::{ilike, nilike};
2443    use arrow_array::{
2444        Array as _, ArrayRef, BooleanArray, Float64Array, StringArray, builder::BooleanBuilder,
2445    };
2446
2447    // Helper: apply a numeric predicate to a single Float64Array chunk
2448    fn apply_numeric_pred(
2449        chunk: &Float64Array,
2450        pred: &crate::args::CriteriaPredicate,
2451    ) -> Option<BooleanArray> {
2452        match pred {
2453            crate::args::CriteriaPredicate::Gt(n) => {
2454                cmp::gt(chunk, &Float64Array::new_scalar(*n)).ok()
2455            }
2456            crate::args::CriteriaPredicate::Ge(n) => {
2457                cmp::gt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
2458            }
2459            crate::args::CriteriaPredicate::Lt(n) => {
2460                cmp::lt(chunk, &Float64Array::new_scalar(*n)).ok()
2461            }
2462            crate::args::CriteriaPredicate::Le(n) => {
2463                cmp::lt_eq(chunk, &Float64Array::new_scalar(*n)).ok()
2464            }
2465            crate::args::CriteriaPredicate::Eq(v) => match v {
2466                formualizer_common::LiteralValue::Number(x) => {
2467                    cmp::eq(chunk, &Float64Array::new_scalar(*x)).ok()
2468                }
2469                formualizer_common::LiteralValue::Int(i) => {
2470                    cmp::eq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
2471                }
2472                _ => None,
2473            },
2474            crate::args::CriteriaPredicate::Ne(v) => match v {
2475                formualizer_common::LiteralValue::Number(x) => {
2476                    cmp::neq(chunk, &Float64Array::new_scalar(*x)).ok()
2477                }
2478                formualizer_common::LiteralValue::Int(i) => {
2479                    cmp::neq(chunk, &Float64Array::new_scalar(*i as f64)).ok()
2480                }
2481                _ => None,
2482            },
2483            _ => None,
2484        }
2485    }
2486
2487    // Check if this is a numeric predicate that can be applied per-chunk
2488    let is_numeric_pred = matches!(
2489        pred,
2490        crate::args::CriteriaPredicate::Gt(_)
2491            | crate::args::CriteriaPredicate::Ge(_)
2492            | crate::args::CriteriaPredicate::Lt(_)
2493            | crate::args::CriteriaPredicate::Le(_)
2494            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Number(_))
2495            | crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Int(_))
2496            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Number(_))
2497            | crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Int(_))
2498    );
2499
2500    // OPTIMIZED PATH: For numeric predicates, apply per-chunk and concatenate boolean masks.
2501    // This avoids materializing the full numeric column (64-bit per element) and instead
2502    // concatenates boolean masks (1-bit per element) - a 64x memory reduction.
2503    if is_numeric_pred {
2504        let mut bool_parts: Vec<BooleanArray> = Vec::new();
2505        // `=n` / `<>n`: the number lane is null for blank, text, boolean and
2506        // error cells. Blanks, booleans and errors never equal a number;
2507        // numeric text does (`5` matches "5"), as in `criteria_match`.
2508        let number = |v: &formualizer_common::LiteralValue| match v {
2509            formualizer_common::LiteralValue::Number(n) => Some(*n),
2510            formualizer_common::LiteralValue::Int(i) => Some(*i as f64),
2511            _ => None,
2512        };
2513        let equality = match pred {
2514            crate::args::CriteriaPredicate::Eq(v) => number(v).map(|n| (n, false)),
2515            crate::args::CriteriaPredicate::Ne(v) => number(v).map(|n| (n, true)),
2516            _ => None,
2517        };
2518        for res in view.numbers_slices() {
2519            let (rs, rl, cols_seg) = res.ok()?;
2520            if col_in_view < cols_seg.len() {
2521                let chunk = cols_seg[col_in_view].as_ref();
2522                let mut mask = apply_numeric_pred(chunk, pred)?;
2523                if let Some((n, ne)) = equality
2524                    && mask.null_count() > 0
2525                {
2526                    mask = fill_numeric_equality_nulls(view, rs, rl, col_in_view, &mask, n, ne)?;
2527                }
2528                bool_parts.push(mask);
2529            }
2530        }
2531
2532        if bool_parts.is_empty() {
2533            return None;
2534        } else if bool_parts.len() == 1 {
2535            return Some(std::sync::Arc::new(bool_parts.remove(0)));
2536        } else {
2537            // Concatenate boolean masks (much cheaper than concatenating Float64 arrays)
2538            let anys: Vec<&dyn arrow_array::Array> = bool_parts
2539                .iter()
2540                .map(|a| a as &dyn arrow_array::Array)
2541                .collect();
2542            let conc: ArrayRef = concat_arrays(&anys).ok()?;
2543            let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2544            return Some(std::sync::Arc::new(ba));
2545        }
2546    }
2547
2548    // Wildcards and numeric text equality can match non-text cells. The lowered
2549    // base lane is text-only, unlike the scalar matcher. Keep the vectorized
2550    // path for text-only data, but cache a scalar-equivalent mask for mixed data.
2551    if is_numeric_text_equality(pred)
2552        || matches!(pred, crate::args::CriteriaPredicate::TextLike { .. })
2553    {
2554        for tags in view.type_tags_slices() {
2555            let (_, _, cols) = tags.ok()?;
2556            let tags = cols.get(col_in_view)?;
2557            if tags.values().iter().any(|tag| {
2558                *tag == crate::arrow_store::TypeTag::Empty as u8
2559                    || *tag == crate::arrow_store::TypeTag::Number as u8
2560                    || *tag == crate::arrow_store::TypeTag::Boolean as u8
2561            }) {
2562                let mut mask = BooleanBuilder::new();
2563                for chunk in view.iter_row_chunks() {
2564                    let chunk = chunk.ok()?;
2565                    for row in chunk.row_start..chunk.row_start + chunk.row_len {
2566                        mask.append_value(crate::builtins::criteria_match(
2567                            pred,
2568                            &view.get_cell(row, col_in_view),
2569                        ));
2570                    }
2571                }
2572                return Some(std::sync::Arc::new(mask.finish()));
2573            }
2574        }
2575    }
2576
2577    // SQL LIKE cannot directly represent spreadsheet tilde escapes or literal
2578    // SQL pattern punctuation. Let the bounded chunk fallback use the shared
2579    // spreadsheet matcher rather than rewriting these patterns into SQL syntax.
2580    if matches!(pred, crate::args::CriteriaPredicate::TextLike { pattern, .. }
2581        if pattern.contains(['~', '%', '_', '\\']))
2582    {
2583        return None;
2584    }
2585
2586    // TEXT PATH: build masks per row-chunk using lowered text slices.
2587    // This avoids concatenating full-string columns just to compute a boolean mask.
2588    // `"="` / `"<>"`: truly blank cells only (empty text is not blank).
2589    if matches!(
2590        pred,
2591        crate::args::CriteriaPredicate::IsBlank | crate::args::CriteriaPredicate::NotBlank
2592    ) {
2593        let want_blank = matches!(pred, crate::args::CriteriaPredicate::IsBlank);
2594        let mut bool_parts: Vec<BooleanArray> = Vec::new();
2595        for tags in view.type_tags_slices() {
2596            let (_, _, cols) = tags.ok()?;
2597            let tags = cols.get(col_in_view)?;
2598            let mut bb = BooleanBuilder::with_capacity(tags.len());
2599            for i in 0..tags.len() {
2600                let blank = tags.value(i) == crate::arrow_store::TypeTag::Empty as u8;
2601                bb.append_value(blank == want_blank);
2602            }
2603            bool_parts.push(bb.finish());
2604        }
2605        return match bool_parts.len() {
2606            0 => None,
2607            1 => Some(std::sync::Arc::new(bool_parts.remove(0))),
2608            _ => {
2609                let anys: Vec<&dyn arrow_array::Array> = bool_parts
2610                    .iter()
2611                    .map(|a| a as &dyn arrow_array::Array)
2612                    .collect();
2613                let conc: ArrayRef = concat_arrays(&anys).ok()?;
2614                let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2615                Some(std::sync::Arc::new(ba))
2616            }
2617        };
2618    }
2619
2620    let (text_kind, text_pat, empty_special) = match pred {
2621        crate::args::CriteriaPredicate::Eq(formualizer_common::LiteralValue::Text(t)) => {
2622            (0u8, t.to_lowercase(), t.is_empty())
2623        }
2624        crate::args::CriteriaPredicate::Ne(formualizer_common::LiteralValue::Text(t)) => {
2625            (1u8, t.to_lowercase(), false)
2626        }
2627        crate::args::CriteriaPredicate::TextLike {
2628            pattern,
2629            case_insensitive,
2630        } => {
2631            let p = if *case_insensitive {
2632                pattern.to_lowercase()
2633            } else {
2634                pattern.clone()
2635            };
2636            (2u8, p.replace('*', "%").replace('?', "_"), false)
2637        }
2638        _ => return None,
2639    };
2640
2641    let text_pat_is_empty = text_pat.is_empty();
2642    let ne_matches_blank = text_kind == 1 && !text_pat_is_empty;
2643    let pat = StringArray::new_scalar(text_pat);
2644    let mut bool_parts: Vec<BooleanArray> = Vec::new();
2645
2646    let mut tag_slices = view.type_tags_slices();
2647    for res in view.iter_row_chunks() {
2648        let cs = res.ok()?;
2649        if cs.row_len == 0 {
2650            continue;
2651        }
2652        #[cfg(test)]
2653        criteria_mask_test_hooks::inc_total();
2654
2655        let slices = view.slice_lowered_text(cs.row_start, cs.row_len);
2656        if col_in_view >= slices.len() {
2657            return None;
2658        }
2659
2660        let seg_opt = slices[col_in_view].as_ref().map(|a| a.as_ref());
2661        if empty_special || (text_kind == 1 && text_pat_is_empty) {
2662            let (tag_start, tag_len, tags) = tag_slices.next()?.ok()?;
2663            if tag_start != cs.row_start || tag_len != cs.row_len {
2664                return None;
2665            }
2666            let tags = tags.get(col_in_view)?;
2667            // A null text lane is not a blank cell: base numeric/boolean/error
2668            // cells also have null text. Consult the overlay-aware type tags,
2669            // and inspect strings only to distinguish empty text from text.
2670            let strings = seg_opt.and_then(|a| a.as_any().downcast_ref::<StringArray>());
2671            let mut bb = BooleanBuilder::with_capacity(cs.row_len);
2672            for i in 0..cs.row_len {
2673                let blank = tags.value(i) == crate::arrow_store::TypeTag::Empty as u8
2674                    || (tags.value(i) == crate::arrow_store::TypeTag::Text as u8
2675                        && strings.is_some_and(|s| s.is_valid(i) && s.value(i).is_empty()));
2676                bb.append_value(if text_kind == 0 { blank } else { !blank });
2677            }
2678            #[cfg(test)]
2679            if seg_opt.is_none() {
2680                criteria_mask_test_hooks::inc_all_null();
2681            }
2682            bool_parts.push(bb.finish());
2683            continue;
2684        }
2685        let seg = match seg_opt {
2686            Some(s) => s,
2687            None => {
2688                #[cfg(test)]
2689                criteria_mask_test_hooks::inc_all_null();
2690                if (text_kind == 0 && empty_special) || ne_matches_blank {
2691                    // Eq("") treats nulls (Empty) as equal.
2692                    let mut bb = BooleanBuilder::with_capacity(cs.row_len);
2693                    bb.append_n(cs.row_len, true);
2694                    bool_parts.push(bb.finish());
2695                } else {
2696                    // For non-empty patterns, ilike/nilike return null on null inputs.
2697                    bool_parts.push(BooleanArray::new_null(cs.row_len));
2698                }
2699                continue;
2700            }
2701        };
2702
2703        let seg_sa = seg.as_any().downcast_ref::<StringArray>()?;
2704        let mut m = match text_kind {
2705            0 => ilike(seg_sa, &pat).ok()?,
2706            1 => nilike(seg_sa, &pat).ok()?,
2707            2 => ilike(seg_sa, &pat).ok()?,
2708            _ => return None,
2709        };
2710
2711        // Only fold blank/Empty (null) cells into the mask when the segment
2712        // actually contains any. The null-fill loop + or_kleene are pure
2713        // overhead on blank-free chunks, so a `<>text` (or `=""`) aggregation
2714        // over a column with no blanks stays fully vectorized on the ilike/
2715        // nilike result.
2716        if ((text_kind == 0 && empty_special) || ne_matches_blank) && seg_sa.null_count() > 0 {
2717            // Treat nulls as equal to empty string
2718            let mut bb = BooleanBuilder::with_capacity(seg_sa.len());
2719            for i in 0..seg_sa.len() {
2720                bb.append_value(seg_sa.is_null(i));
2721            }
2722            let nulls = bb.finish();
2723            m = boolean::or_kleene(&m, &nulls).ok()?;
2724        }
2725
2726        bool_parts.push(m);
2727    }
2728
2729    if bool_parts.is_empty() {
2730        None
2731    } else if bool_parts.len() == 1 {
2732        Some(std::sync::Arc::new(bool_parts.remove(0)))
2733    } else {
2734        let anys: Vec<&dyn arrow_array::Array> = bool_parts
2735            .iter()
2736            .map(|a| a as &dyn arrow_array::Array)
2737            .collect();
2738        let conc: ArrayRef = concat_arrays(&anys).ok()?;
2739        let ba = conc.as_any().downcast_ref::<BooleanArray>()?.clone();
2740        Some(std::sync::Arc::new(ba))
2741    }
2742}
2743
2744#[derive(Debug, Clone)]
2745pub struct LayerInfo {
2746    pub vertex_count: usize,
2747    pub parallel_eligible: bool,
2748    pub sample_cells: Vec<String>, // Sample of up to 5 cell addresses
2749}
2750
2751#[derive(Debug, Clone)]
2752pub struct EvalPlan {
2753    pub total_vertices_to_evaluate: usize,
2754    pub layers: Vec<LayerInfo>,
2755    pub cycles_detected: usize,
2756    pub dirty_count: usize,
2757    pub volatile_count: usize,
2758    pub parallel_enabled: bool,
2759    pub estimated_parallel_layers: usize,
2760    pub target_cells: Vec<String>,
2761}
2762
2763/// Whether the configured FormulaPlane mode is ignored (spans never placed):
2764/// always, since the dependency authority is the runtime path (design §10).
2765#[inline]
2766fn plane_mode_ignored() -> bool {
2767    true
2768}
2769
2770/// Test-support probe for sibling-crate tests: true when span placement is
2771/// off because the FormulaPlane mode is ignored (see `plane_mode_ignored`).
2772#[cfg(feature = "test-support")]
2773#[doc(hidden)]
2774pub fn formula_plane_mode_ignored_for_test() -> bool {
2775    plane_mode_ignored()
2776}
2777
2778impl<R> Engine<R>
2779where
2780    R: EvaluationContext,
2781{
2782    /// # Panics
2783    /// Panics when `config.cycle` is invalid ([`CycleConfig::validate`],
2784    /// spec §2): `Iterate` with `detection: Static`, `max_iterations == 0`,
2785    /// or a negative/non-finite `max_change`. `EvalConfig::with_cycle`
2786    /// rejects these at build; this re-validates configs assembled via
2787    /// struct literals.
2788    pub fn new(resolver: R, config: EvalConfig) -> Self {
2789        // Under the unified authority the FormulaPlane mode is accepted and
2790        // ignored (design §10). Normalizing the stored mode keeps external
2791        // readers of `config` (loaders choosing a span-preparation route) on
2792        // the per-cell path; engine reads go through `formula_plane_mode()`.
2793        let config = if plane_mode_ignored() {
2794            EvalConfig {
2795                formula_plane_mode: FormulaPlaneMode::Off,
2796                ..config
2797            }
2798        } else {
2799            config
2800        };
2801        if let Err(msg) = config.cycle.validate() {
2802            panic!("invalid CycleConfig: {msg}");
2803        }
2804        crate::builtins::load_builtins();
2805        let resolved_resources = crate::engine::resource_ledger::resolve_evaluation_budgets(
2806            &config.evaluation_budgets,
2807            config.max_vertices,
2808            config.max_memory_mb,
2809            config.max_eval_time,
2810        );
2811
2812        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
2813            #[cfg(feature = "system-clock")]
2814            {
2815                Arc::new(crate::timezone::SystemClock::new(
2816                    crate::timezone::TimeZoneSpec::default(),
2817                ))
2818            }
2819            #[cfg(not(feature = "system-clock"))]
2820            {
2821                Arc::new(crate::timezone::FixedClock::new(
2822                    chrono::DateTime::UNIX_EPOCH,
2823                    crate::timezone::TimeZoneSpec::Utc,
2824                ))
2825            }
2826        });
2827
2828        // Initialize thread pool based on config
2829        let thread_pool = if config.enable_parallel {
2830            let mut builder = ThreadPoolBuilder::new();
2831            if let Some(max_threads) = config.max_threads {
2832                builder = builder.num_threads(max_threads);
2833            }
2834
2835            match builder.build() {
2836                Ok(pool) => Some(Arc::new(pool)),
2837                Err(_) => {
2838                    // Fall back to sequential evaluation if thread pool creation fails
2839                    None
2840                }
2841            }
2842        } else {
2843            None
2844        };
2845
2846        // C1a retained/cache budgets are observational; cache defaults stay explicit.
2847        let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
2848        let function_provider_revision_seen = resolver.planning_semantic_revision();
2849        let mut engine = Self {
2850            graph: DependencyGraph::new_with_config(config.clone()),
2851            resolver,
2852            config,
2853            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
2854            clock: crate::timezone::SnapshotClock::new(clock),
2855            thread_pool,
2856            recalc_epoch: 0,
2857            snapshot_id: std::sync::atomic::AtomicU64::new(1),
2858            topology_epoch: 0,
2859            cached_static_schedule: None,
2860            recent_schedules: Vec::new(),
2861            base_schedule: None,
2862            #[cfg(any(test, feature = "benchmark_internal"))]
2863            recalc_reuse_probe: std::sync::Mutex::new(RecalcReuseProbe::default()),
2864            spill_mgr: ShimSpillManager::default(),
2865            arrow_sheets: SheetStore::default(),
2866            format_registry: crate::format::FormatRegistry::default(),
2867            derived_formats: Default::default(),
2868            #[cfg(test)]
2869            derived_format_operations_for_test: std::sync::atomic::AtomicU64::new(0),
2870            #[cfg(test)]
2871            family_members_for_test: std::sync::atomic::AtomicU64::new(0),
2872            #[cfg(test)]
2873            invariant_bound_members_for_test: std::sync::atomic::AtomicU64::new(0),
2874            #[cfg(test)]
2875            lifted_members_for_test: std::sync::atomic::AtomicU64::new(0),
2876            #[cfg(test)]
2877            chained_members_for_test: std::sync::atomic::AtomicU64::new(0),
2878            #[cfg(test)]
2879            lane_clean_reads_for_test: std::sync::atomic::AtomicU64::new(0),
2880            #[cfg(test)]
2881            criteria_kernel_members_for_test: std::sync::atomic::AtomicU64::new(0),
2882            #[cfg(test)]
2883            memo_hits_for_test: std::sync::atomic::AtomicU64::new(0),
2884            compressed_at_build: None,
2885            #[cfg(test)]
2886            computed_overlay_set_explicit_entry_operations_for_test: 0,
2887            #[cfg(test)]
2888            computed_overlay_stale_clear_range_effects_for_test: 0,
2889            #[cfg(test)]
2890            computed_overlay_stale_clear_offset_attempts_for_test: 0,
2891            #[cfg(test)]
2892            computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64::new(0),
2893            has_edited: false,
2894            overlay_compactions: 0,
2895            computed_overlay_bytes_estimate: 0,
2896            computed_overlay_mirroring_disabled: false,
2897            force_materialize_range_views: false,
2898            row_bounds_cache: std::sync::RwLock::new(None),
2899            used_axis_bounds_cache: std::sync::RwLock::new(None),
2900            lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
2901            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
2902            source_formula_token: Arc::new(()),
2903            recalc_plan_token: Arc::new(()),
2904            staged_formulas: std::collections::HashMap::new(),
2905            staged_formula_index: StagedFormulaIndex::default(),
2906            blocked_pending_spills: Vec::new(),
2907            spill_blocker_waiters: SpillBlockerWaiters::default(),
2908            row_visibility: FxHashMap::default(),
2909            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
2910            formula_parse_diagnostics: Vec::new(),
2911            last_formula_ingest_report: None,
2912            formula_ingest_report_total: FormulaIngestReport::default(),
2913            active_cancel_flag: None,
2914            active_evaluation_deadline: None,
2915            action_depth: 0,
2916            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
2917            virtual_dep_fallback_activations: 0,
2918            last_cycle_telemetry: CycleTelemetry::default(),
2919            next_evaluation_resource_request_id: 1,
2920            evaluation_resource_request_depth: 0,
2921            active_evaluation_resource_request: None,
2922            last_evaluation_resource_request: None,
2923            evaluation_resource_baseline: EvaluationResourceBaselineStats::default(),
2924            evaluation_resource_request_started_at: None,
2925            evaluation_resource_budgets: resolved_resources.budgets,
2926            evaluation_resource_config_diagnostic: resolved_resources.diagnostic,
2927            active_resource_ledger: None,
2928            source_cache_footprints: Vec::new(),
2929            source_cache_accounted: 0,
2930            pending_iterative_redirty: Vec::new(),
2931            snapshot_evaluation_active: false,
2932            snapshot_volatile_redirty_pending: false,
2933            retained_scc_members: FxHashMap::default(),
2934            next_retained_scc_id: 0,
2935            retained_scc_config_fingerprint: 0,
2936            retained_scc_function_epoch_seen: 0,
2937            retained_scc_provider_revision_seen: None,
2938            retained_scc_dirty_at_begin: Vec::new(),
2939            iterative_state_values: FxHashMap::default(),
2940            function_semantic_epoch_seen: crate::function_registry::semantic_epoch(),
2941            function_provider_revision_seen,
2942            #[cfg(feature = "tracing")]
2943            trace_evaluation_counters: TraceEvaluationCounters::default(),
2944            #[cfg(test)]
2945            evaluation_request_begin_count_for_test: 0,
2946            #[cfg(any(test, feature = "test-support"))]
2947            before_prepared_span_commit_hook: None,
2948            #[cfg(test)]
2949            before_target_preparation_commit_hook: None,
2950            #[cfg(test)]
2951            before_target_planning_snapshot_hook: None,
2952            #[cfg(test)]
2953            inject_target_semantic_stale_once_for_test: false,
2954            #[cfg(test)]
2955            force_virtual_dep_changes_remaining_for_test: 0,
2956            freshness: Default::default(),
2957            #[cfg(test)]
2958            fail_evaluation_commit_preflight_once_for_test: false,
2959            #[cfg(test)]
2960            target_preparation_fault_for_test: None,
2961            #[cfg(test)]
2962            force_non_cycle_schedule_fallback_for_test: false,
2963            #[cfg(test)]
2964            before_legacy_fallback_final_provider_sample_hook: None,
2965            #[cfg(test)]
2966            after_eager_proposal_commit_hook: None,
2967        };
2968        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
2969        engine.config.arrow_storage_enabled = true;
2970        engine.config.delta_overlay_enabled = true;
2971        engine.config.write_formula_overlay_enabled = true;
2972        let default_sheet = engine.graph.default_sheet_name().to_string();
2973        engine.ensure_arrow_sheet(&default_sheet);
2974        engine
2975    }
2976
2977    /// Create an Engine with a custom thread pool (for shared thread pool scenarios)
2978    ///
2979    /// # Panics
2980    /// Panics when `config.cycle` is invalid, exactly like [`Engine::new`].
2981    pub fn with_thread_pool(
2982        resolver: R,
2983        config: EvalConfig,
2984        thread_pool: Arc<rayon::ThreadPool>,
2985    ) -> Self {
2986        if let Err(msg) = config.cycle.validate() {
2987            panic!("invalid CycleConfig: {msg}");
2988        }
2989        crate::builtins::load_builtins();
2990        let resolved_resources = crate::engine::resource_ledger::resolve_evaluation_budgets(
2991            &config.evaluation_budgets,
2992            config.max_vertices,
2993            config.max_memory_mb,
2994            config.max_eval_time,
2995        );
2996        let clock = config.deterministic_mode.build_clock().unwrap_or_else(|_| {
2997            #[cfg(feature = "system-clock")]
2998            {
2999                Arc::new(crate::timezone::SystemClock::new(
3000                    crate::timezone::TimeZoneSpec::default(),
3001                ))
3002            }
3003            #[cfg(not(feature = "system-clock"))]
3004            {
3005                Arc::new(crate::timezone::FixedClock::new(
3006                    chrono::DateTime::UNIX_EPOCH,
3007                    crate::timezone::TimeZoneSpec::Utc,
3008                ))
3009            }
3010        });
3011        // C1a retained/cache budgets are observational; cache defaults stay explicit.
3012        let lookup_cache_max_bytes = config.lookup_index_cache_max_bytes;
3013        let function_provider_revision_seen = resolver.planning_semantic_revision();
3014        let mut engine = Self {
3015            graph: DependencyGraph::new_with_config(config.clone()),
3016            resolver,
3017            config,
3018            workbook_load_limits: crate::engine::WorkbookLoadLimits::default(),
3019            clock: crate::timezone::SnapshotClock::new(clock),
3020            thread_pool: Some(thread_pool),
3021            recalc_epoch: 0,
3022            snapshot_id: std::sync::atomic::AtomicU64::new(1),
3023            topology_epoch: 0,
3024            cached_static_schedule: None,
3025            recent_schedules: Vec::new(),
3026            base_schedule: None,
3027            #[cfg(any(test, feature = "benchmark_internal"))]
3028            recalc_reuse_probe: std::sync::Mutex::new(RecalcReuseProbe::default()),
3029            spill_mgr: ShimSpillManager::default(),
3030            arrow_sheets: SheetStore::default(),
3031            format_registry: crate::format::FormatRegistry::default(),
3032            derived_formats: Default::default(),
3033            #[cfg(test)]
3034            derived_format_operations_for_test: std::sync::atomic::AtomicU64::new(0),
3035            #[cfg(test)]
3036            family_members_for_test: std::sync::atomic::AtomicU64::new(0),
3037            #[cfg(test)]
3038            invariant_bound_members_for_test: std::sync::atomic::AtomicU64::new(0),
3039            #[cfg(test)]
3040            lifted_members_for_test: std::sync::atomic::AtomicU64::new(0),
3041            #[cfg(test)]
3042            chained_members_for_test: std::sync::atomic::AtomicU64::new(0),
3043            #[cfg(test)]
3044            lane_clean_reads_for_test: std::sync::atomic::AtomicU64::new(0),
3045            #[cfg(test)]
3046            criteria_kernel_members_for_test: std::sync::atomic::AtomicU64::new(0),
3047            #[cfg(test)]
3048            memo_hits_for_test: std::sync::atomic::AtomicU64::new(0),
3049            compressed_at_build: None,
3050            #[cfg(test)]
3051            computed_overlay_set_explicit_entry_operations_for_test: 0,
3052            #[cfg(test)]
3053            computed_overlay_stale_clear_range_effects_for_test: 0,
3054            #[cfg(test)]
3055            computed_overlay_stale_clear_offset_attempts_for_test: 0,
3056            #[cfg(test)]
3057            computed_format_vector_allocations_for_test: std::sync::atomic::AtomicU64::new(0),
3058            has_edited: false,
3059            overlay_compactions: 0,
3060            computed_overlay_bytes_estimate: 0,
3061            computed_overlay_mirroring_disabled: false,
3062            force_materialize_range_views: false,
3063            row_bounds_cache: std::sync::RwLock::new(None),
3064            used_axis_bounds_cache: std::sync::RwLock::new(None),
3065            lookup_index_cache: LookupIndexCache::new(lookup_cache_max_bytes),
3066            source_cache: Arc::new(std::sync::RwLock::new(SourceCache::default())),
3067            source_formula_token: Arc::new(()),
3068            recalc_plan_token: Arc::new(()),
3069            staged_formulas: std::collections::HashMap::new(),
3070            staged_formula_index: StagedFormulaIndex::default(),
3071            blocked_pending_spills: Vec::new(),
3072            spill_blocker_waiters: SpillBlockerWaiters::default(),
3073            row_visibility: FxHashMap::default(),
3074            row_visibility_mask_cache: std::sync::RwLock::new(FxHashMap::default()),
3075            formula_parse_diagnostics: Vec::new(),
3076            last_formula_ingest_report: None,
3077            formula_ingest_report_total: FormulaIngestReport::default(),
3078            active_cancel_flag: None,
3079            active_evaluation_deadline: None,
3080            action_depth: 0,
3081            last_virtual_dep_telemetry: VirtualDepTelemetry::default(),
3082            virtual_dep_fallback_activations: 0,
3083            last_cycle_telemetry: CycleTelemetry::default(),
3084            next_evaluation_resource_request_id: 1,
3085            evaluation_resource_request_depth: 0,
3086            active_evaluation_resource_request: None,
3087            last_evaluation_resource_request: None,
3088            evaluation_resource_baseline: EvaluationResourceBaselineStats::default(),
3089            evaluation_resource_request_started_at: None,
3090            evaluation_resource_budgets: resolved_resources.budgets,
3091            evaluation_resource_config_diagnostic: resolved_resources.diagnostic,
3092            active_resource_ledger: None,
3093            source_cache_footprints: Vec::new(),
3094            source_cache_accounted: 0,
3095            pending_iterative_redirty: Vec::new(),
3096            snapshot_evaluation_active: false,
3097            snapshot_volatile_redirty_pending: false,
3098            retained_scc_members: FxHashMap::default(),
3099            next_retained_scc_id: 0,
3100            retained_scc_config_fingerprint: 0,
3101            retained_scc_function_epoch_seen: 0,
3102            retained_scc_provider_revision_seen: None,
3103            retained_scc_dirty_at_begin: Vec::new(),
3104            iterative_state_values: FxHashMap::default(),
3105            function_semantic_epoch_seen: crate::function_registry::semantic_epoch(),
3106            function_provider_revision_seen,
3107            #[cfg(feature = "tracing")]
3108            trace_evaluation_counters: TraceEvaluationCounters::default(),
3109            #[cfg(test)]
3110            evaluation_request_begin_count_for_test: 0,
3111            #[cfg(any(test, feature = "test-support"))]
3112            before_prepared_span_commit_hook: None,
3113            #[cfg(test)]
3114            before_target_preparation_commit_hook: None,
3115            #[cfg(test)]
3116            before_target_planning_snapshot_hook: None,
3117            #[cfg(test)]
3118            inject_target_semantic_stale_once_for_test: false,
3119            #[cfg(test)]
3120            force_virtual_dep_changes_remaining_for_test: 0,
3121            freshness: Default::default(),
3122            #[cfg(test)]
3123            fail_evaluation_commit_preflight_once_for_test: false,
3124            #[cfg(test)]
3125            target_preparation_fault_for_test: None,
3126            #[cfg(test)]
3127            force_non_cycle_schedule_fallback_for_test: false,
3128            #[cfg(test)]
3129            before_legacy_fallback_final_provider_sample_hook: None,
3130            #[cfg(test)]
3131            after_eager_proposal_commit_hook: None,
3132        };
3133        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
3134        engine.config.arrow_storage_enabled = true;
3135        engine.config.delta_overlay_enabled = true;
3136        engine.config.write_formula_overlay_enabled = true;
3137        let default_sheet = engine.graph.default_sheet_name().to_string();
3138        engine.ensure_arrow_sheet(&default_sheet);
3139        engine
3140    }
3141
3142    pub fn workbook_load_limits(&self) -> &crate::engine::WorkbookLoadLimits {
3143        &self.workbook_load_limits
3144    }
3145
3146    pub fn set_workbook_load_limits(&mut self, limits: crate::engine::WorkbookLoadLimits) {
3147        self.workbook_load_limits = limits;
3148    }
3149
3150    fn clear_source_cache(&self) {
3151        if let Ok(mut g) = self.source_cache.write() {
3152            *g = SourceCache::default();
3153        }
3154    }
3155
3156    pub fn last_virtual_dep_telemetry(&self) -> &VirtualDepTelemetry {
3157        &self.last_virtual_dep_telemetry
3158    }
3159
3160    /// Telemetry from Runtime SCC evaluation during the most recent
3161    /// evaluation request (always default-zero under `CycleDetection::Static`
3162    /// or when `enable_virtual_dep_telemetry` is off).
3163    pub fn last_cycle_telemetry(&self) -> &CycleTelemetry {
3164        &self.last_cycle_telemetry
3165    }
3166
3167    /// Resource observations for the most recently completed public evaluation request.
3168    pub fn last_evaluation_resource_request_stats(
3169        &self,
3170    ) -> Option<&EvaluationResourceRequestStats> {
3171        self.last_evaluation_resource_request.as_ref()
3172    }
3173
3174    /// Cumulative resource observations since engine creation or the last telemetry reset.
3175    pub fn evaluation_resource_baseline_stats(&self) -> EvaluationResourceBaselineStats {
3176        self.evaluation_resource_baseline
3177    }
3178
3179    pub fn evaluation_resource_budgets(&self) -> &crate::engine::EvaluationBudgets {
3180        &self.evaluation_resource_budgets
3181    }
3182
3183    /// At most one diagnostic is emitted for deprecated resource fields.
3184    pub fn evaluation_resource_config_diagnostic(
3185        &self,
3186    ) -> Option<&crate::engine::EvaluationResourceConfigDiagnostic> {
3187        self.evaluation_resource_config_diagnostic.as_ref()
3188    }
3189
3190    /// Reset accumulated and last-request observations without reusing request IDs.
3191    pub fn reset_evaluation_resource_telemetry(&mut self) {
3192        self.evaluation_resource_baseline = EvaluationResourceBaselineStats::default();
3193        self.last_evaluation_resource_request = None;
3194    }
3195
3196    // Reconcile without replay locks: several packages may share the same Arc.
3197    // Weak tokens neither retain dead packages nor duplicate their allocations.
3198    fn reconcile_source_cache_footprints(&mut self) -> Result<(), ExcelError> {
3199        self.blocked_pending_spills.retain(|&(vertex, anchor, _)| {
3200            self.graph.vertex_exists(vertex)
3201                && self.graph.get_cell_ref(vertex) == Some(anchor)
3202                && matches!(
3203                    self.graph.get_vertex_kind(vertex),
3204                    VertexKind::FormulaScalar | VertexKind::FormulaArray
3205                )
3206        });
3207        if self.blocked_pending_spills.is_empty() {
3208            self.blocked_pending_spills = Vec::new();
3209        }
3210        let mut bytes = (self.blocked_pending_spills.capacity()
3211            * std::mem::size_of::<(VertexId, CellRef, Region)>()) as u64;
3212        self.source_cache_footprints.retain(|weak| {
3213            let Some(footprint) = weak.upgrade() else {
3214                return false;
3215            };
3216            bytes = bytes.saturating_add(footprint.load(std::sync::atomic::Ordering::Acquire));
3217            true
3218        });
3219        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3220            ledger
3221                .release_retained(self.source_cache_accounted)
3222                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3223            // Observe first: even a tightened budget cannot erase live ownership.
3224            ledger.observe_retained(bytes);
3225            self.source_cache_accounted = bytes;
3226            ledger
3227                .reserve_retained(0)
3228                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3229        }
3230        Ok(())
3231    }
3232
3233    fn duration_ns(duration: std::time::Duration) -> u64 {
3234        u64::try_from(duration.as_nanos()).unwrap_or(u64::MAX)
3235    }
3236
3237    fn observe_evaluation_resource_request<T>(
3238        &mut self,
3239        kind: EvaluationRequestKind,
3240        evaluate: impl FnOnce(&mut Self) -> Result<T, ExcelError>,
3241    ) -> Result<T, ExcelError> {
3242        let outermost = self.evaluation_resource_request_depth == 0;
3243        if outermost && self.snapshot_volatile_redirty_pending {
3244            self.snapshot_volatile_redirty_pending = false;
3245            self.graph.redirty_volatiles();
3246        }
3247        #[cfg(feature = "tracing")]
3248        if outermost {
3249            self.trace_evaluation_counters = TraceEvaluationCounters::default();
3250        }
3251        #[cfg(feature = "tracing")]
3252        let request_kind = if matches!(
3253            kind,
3254            EvaluationRequestKind::Full
3255                | EvaluationRequestKind::FullWithDelta
3256                | EvaluationRequestKind::FullCancellable
3257                | EvaluationRequestKind::FullLogged
3258        ) {
3259            "full"
3260        } else {
3261            "targeted"
3262        };
3263        #[cfg(feature = "tracing")]
3264        let _request_span = outermost.then(|| {
3265            crate::engine::trace::fz_span!(
3266                tracing::Level::INFO,
3267                "evaluate",
3268                "evaluate.request",
3269                kind = request_kind,
3270                mode = ?FormulaPlaneMode::Off
3271            )
3272        });
3273        if outermost {
3274            let request_id = self.next_evaluation_resource_request_id;
3275            self.next_evaluation_resource_request_id = request_id
3276                .checked_add(1)
3277                .expect("evaluation resource request ID exhausted");
3278            self.active_evaluation_resource_request = Some(EvaluationResourceRequestStats::new(
3279                request_id,
3280                kind,
3281                FormulaPlaneMode::Off,
3282                self.staged_formula_count(),
3283            ));
3284            self.evaluation_resource_baseline.record_started(request_id);
3285            self.evaluation_resource_request_started_at = Some(crate::instant::FzInstant::now());
3286            self.source_cache_accounted = 0;
3287            self.active_resource_ledger = Some(ResourceLedger::new(
3288                Some(request_id),
3289                self.evaluation_resource_budgets.clone(),
3290            ));
3291        }
3292        self.evaluation_resource_request_depth =
3293            self.evaluation_resource_request_depth.saturating_add(1);
3294        let result = if outermost {
3295            self.reconcile_source_cache_footprints()
3296                .and_then(|()| self.resource_checkpoint(0))
3297                .and_then(|()| evaluate(self))
3298        } else {
3299            evaluate(self)
3300        };
3301        if outermost && result.is_err() {
3302            self.freshness_abort_pass();
3303        }
3304        self.evaluation_resource_request_depth =
3305            self.evaluation_resource_request_depth.saturating_sub(1);
3306
3307        let reconciliation = if outermost {
3308            self.reconcile_source_cache_footprints()
3309        } else {
3310            Ok(())
3311        };
3312        let result = result.and_then(|value| reconciliation.map(|()| value));
3313        if outermost {
3314            let total_ns = self
3315                .evaluation_resource_request_started_at
3316                .take()
3317                .map(|start| Self::duration_ns(start.elapsed()))
3318                .unwrap_or(0);
3319            let mut stats = self
3320                .active_evaluation_resource_request
3321                .take()
3322                .expect("outer evaluation resource request has active stats");
3323            let mut ledger = self
3324                .active_resource_ledger
3325                .take()
3326                .expect("outer evaluation resource request has active ledger");
3327            ledger.release_all_scratch();
3328            stats.ledger.update(ledger.snapshot());
3329            stats.outcome = match &result {
3330                Ok(_) => EvaluationRequestOutcome::Success,
3331                Err(error) if error.kind == ExcelErrorKind::Cancelled => {
3332                    EvaluationRequestOutcome::Cancelled
3333                }
3334                Err(_) => EvaluationRequestOutcome::Error,
3335            };
3336            if stats.dirty_lease == FormulaDirtyLeaseOutcome::Acquired {
3337                stats.dirty_lease = if stats.outcome == EvaluationRequestOutcome::Cancelled {
3338                    FormulaDirtyLeaseOutcome::RetainedOnCancellation
3339                } else {
3340                    FormulaDirtyLeaseOutcome::RetainedOnError
3341                };
3342            }
3343            stats.phases.total_ns = total_ns;
3344            let attributed = stats
3345                .phases
3346                .staged_prepare_ns
3347                .saturating_add(stats.phases.topology_ns)
3348                .saturating_add(stats.phases.materialization_ns);
3349            stats.phases.evaluation_ns = total_ns.saturating_sub(attributed);
3350            self.evaluation_resource_baseline.record_finished(&stats);
3351            self.last_evaluation_resource_request = Some(stats);
3352            crate::engine::trace::fz_event!(
3353                tracing::Level::INFO,
3354                "evaluate",
3355                "evaluate.summary",
3356                computed_vertices = self.trace_evaluation_counters.computed_vertices,
3357                cycles = self.trace_evaluation_counters.cycles,
3358                cancelled = matches!(
3359                    &result,
3360                    Err(error) if error.kind == ExcelErrorKind::Cancelled
3361                )
3362            );
3363        }
3364        result
3365    }
3366
3367    pub fn set_evaluation_resource_budgets(&mut self, budgets: crate::engine::EvaluationBudgets) {
3368        self.evaluation_resource_budgets = budgets.clone();
3369        self.config.evaluation_budgets = budgets.clone();
3370        self.graph.set_evaluation_budgets(budgets);
3371    }
3372
3373    #[cfg(test)]
3374    pub(crate) fn set_evaluation_budgets_for_test(
3375        &mut self,
3376        budgets: crate::engine::EvaluationBudgets,
3377    ) {
3378        self.set_evaluation_resource_budgets(budgets);
3379    }
3380
3381    fn preflight_evaluation_commit_window(
3382        &mut self,
3383        bounded_writes: usize,
3384    ) -> Result<crate::instant::FzInstant, ExcelError> {
3385        #[cfg(test)]
3386        if std::mem::take(&mut self.fail_evaluation_commit_preflight_once_for_test) {
3387            return Err(crate::engine::ResourceLedgerError::Exhausted(
3388                formualizer_common::ResourceExhaustionDetail {
3389                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
3390                    limit: 0,
3391                    observed: 1,
3392                    request_id: self
3393                        .active_evaluation_resource_request
3394                        .as_ref()
3395                        .map(|stats| stats.request_id),
3396                },
3397            )
3398            .into_excel_error());
3399        }
3400        let estimate = std::time::Duration::from_nanos(
3401            u64::try_from(bounded_writes)
3402                .unwrap_or(u64::MAX)
3403                .saturating_mul(100),
3404        );
3405        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3406            ledger
3407                .preflight_commit_window(estimate)
3408                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
3409        }
3410        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3411            stats.evaluation_commit_preflight_count =
3412                stats.evaluation_commit_preflight_count.saturating_add(1);
3413            stats.evaluation_commit_estimated_ns = stats
3414                .evaluation_commit_estimated_ns
3415                .saturating_add(Self::duration_ns(estimate));
3416        }
3417        Ok(crate::instant::FzInstant::now())
3418    }
3419
3420    fn observe_evaluation_commit_window(&mut self, started: crate::instant::FzInstant) {
3421        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3422            stats.evaluation_commit_actual_ns = stats
3423                .evaluation_commit_actual_ns
3424                .saturating_add(Self::duration_ns(started.elapsed()));
3425        }
3426    }
3427
3428    /// Post-work cancellation boundary: call after evaluating a unit (or a
3429    /// parallel group) and before committing it. A function that observed
3430    /// the request's token returns `Err(Cancelled)`, which the evaluator
3431    /// turns into a `#CANCELLED` value; any other result computed across the
3432    /// signal is equally not a finished result. Neither may publish: the
3433    /// caller returns this error before committing, so the unit stays dirty,
3434    /// the request reports `Cancelled`, and a failed pass restores the
3435    /// vertices it already committed (`freshness_abort_pass`). The token is
3436    /// the discriminator: a `#CANCELLED` value with no live cancellation is
3437    /// ordinary data and commits. Deadlines are not checked here; finished
3438    /// work is not discarded on a deadline.
3439    fn live_cancellation_after_work(&self, message: &'static str) -> Result<(), ExcelError> {
3440        if self
3441            .active_cancel_flag
3442            .as_ref()
3443            .is_some_and(|cancel| cancel.is_cancelled())
3444        {
3445            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(message));
3446        }
3447        Ok(())
3448    }
3449
3450    fn cancellation_checkpoint(&self, message: &'static str) -> Result<(), ExcelError> {
3451        if self
3452            .active_cancel_flag
3453            .as_ref()
3454            .is_some_and(|cancel| cancel.is_cancelled())
3455        {
3456            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(message));
3457        }
3458        if self
3459            .active_evaluation_deadline
3460            .is_some_and(|deadline| Instant::now() >= deadline)
3461        {
3462            return Err(crate::engine::ResourceLedgerError::Exhausted(
3463                formualizer_common::ResourceExhaustionDetail {
3464                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
3465                    limit: 0,
3466                    observed: 1,
3467                    request_id: self
3468                        .active_evaluation_resource_request
3469                        .as_ref()
3470                        .map(|request| request.request_id),
3471                },
3472            )
3473            .into_excel_error()
3474            .with_message(message));
3475        }
3476        Ok(())
3477    }
3478
3479    fn resource_checkpoint(&mut self, work_units: u64) -> Result<(), ExcelError> {
3480        let Some(ledger) = self.active_resource_ledger.as_mut() else {
3481            return Ok(());
3482        };
3483        ledger
3484            .charge_work(work_units)
3485            .and_then(|()| ledger.checkpoint_deadline())
3486            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3487    }
3488
3489    fn charge_bounded_work(&mut self, mut work_units: u64) -> Result<(), ExcelError> {
3490        if work_units == 0 {
3491            return self.resource_checkpoint(0);
3492        }
3493        while work_units > 0 {
3494            let chunk = work_units.min(256);
3495            self.resource_checkpoint(chunk)?;
3496            work_units -= chunk;
3497        }
3498        Ok(())
3499    }
3500
3501    fn reserve_request_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3502        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3503            // Exact request topology and activation of the scratch cap are C1b. C1a records
3504            // scoped ownership but must not introduce a new skip or terminal path.
3505            ledger.observe_scratch(bytes);
3506        }
3507        Ok(())
3508    }
3509
3510    fn release_request_scratch(&mut self, bytes: u64) {
3511        if let Some(ledger) = self.active_resource_ledger.as_mut() {
3512            let released = ledger.release_scratch(bytes);
3513            debug_assert!(
3514                released.is_ok(),
3515                "request scratch release exceeded the outstanding reservation"
3516            );
3517        }
3518    }
3519
3520    fn reserve_topology_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3521        self.active_resource_ledger
3522            .as_mut()
3523            .map_or(Ok(()), |ledger| ledger.reserve_schedule_discovery(bytes))
3524            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3525    }
3526
3527    fn reserve_graph_source_scratch(&mut self, bytes: u64) -> Result<(), ExcelError> {
3528        self.active_resource_ledger
3529            .as_mut()
3530            .map_or(Ok(()), |ledger| ledger.reserve_graph_source(bytes))
3531            .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3532    }
3533
3534    fn with_request_scratch<T>(
3535        &mut self,
3536        bytes: u64,
3537        work: impl FnOnce(&mut Self) -> Result<T, ExcelError>,
3538    ) -> Result<T, ExcelError> {
3539        self.reserve_request_scratch(bytes)?;
3540        let result = work(self);
3541        self.release_request_scratch(bytes);
3542        result
3543    }
3544
3545    fn observe_staged_preparation(
3546        &mut self,
3547        selected: usize,
3548        retained: usize,
3549        elapsed: std::time::Duration,
3550    ) {
3551        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3552            stats.staged_selected = stats.staged_selected.saturating_add(selected as u64);
3553            stats.staged_retained = retained as u64;
3554            stats.phases.staged_prepare_ns = stats
3555                .phases
3556                .staged_prepare_ns
3557                .saturating_add(Self::duration_ns(elapsed));
3558        }
3559    }
3560
3561    fn graph_admission_enabled(&self) -> bool {
3562        crate::engine::resource_ledger::graph_admission_enabled(&self.evaluation_resource_budgets)
3563    }
3564
3565    fn preflight_graph_admission(
3566        &mut self,
3567        usage: crate::engine::resource_ledger::GraphAdmission,
3568    ) -> Result<(), ExcelError> {
3569        let request_id = self
3570            .active_evaluation_resource_request
3571            .as_ref()
3572            .map(|stats| stats.request_id);
3573        crate::engine::resource_ledger::preflight_graph_admission(
3574            &self.evaluation_resource_budgets,
3575            usage,
3576            request_id,
3577        )
3578        .map_err(crate::engine::ResourceLedgerError::into_excel_error)
3579    }
3580
3581    fn prepared_legacy_admission(
3582        &mut self,
3583        plan: &PreparedLegacyGraphPlan,
3584        materialization_cells: u64,
3585    ) -> Result<(), ExcelError> {
3586        if !self.graph_admission_enabled() {
3587            return Ok(());
3588        }
3589        let stats = self.graph.baseline_stats();
3590        let added_edges = plan.planned_edge_count().ok_or_else(|| {
3591            ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
3592        })?;
3593        let removed_edges = plan.removed_edge_count().ok_or_else(|| {
3594            ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
3595        })?;
3596        self.preflight_graph_admission(crate::engine::resource_ledger::GraphAdmission {
3597            final_vertices: stats
3598                .graph_vertex_count
3599                .checked_add(plan.new_vertex_count())
3600                .ok_or_else(|| {
3601                    ExcelError::new(ExcelErrorKind::NImpl)
3602                        .with_message("graph vertex count overflow")
3603                })?,
3604            final_edges: stats
3605                .graph_edge_count
3606                .checked_sub(removed_edges)
3607                .and_then(|count| count.checked_add(added_edges))
3608                .ok_or_else(|| {
3609                    ExcelError::new(ExcelErrorKind::NImpl).with_message("graph edge count overflow")
3610                })?,
3611            materialization_cells,
3612            added_vertices: plan.new_vertex_count(),
3613            added_edges,
3614        })
3615    }
3616
3617    fn observe_target_admission_failure(
3618        &mut self,
3619        reason: formualizer_common::ResourceExhaustionReason,
3620    ) {
3621        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3622            stats.target_admission_failure = Some(reason);
3623        }
3624    }
3625
3626    fn observe_target_preparation_report(
3627        &mut self,
3628        report: &crate::engine::PreparedTargetGraphReport,
3629    ) {
3630        let reason_bit = |reason: crate::engine::OpaqueReason| -> u64 {
3631            let index = match reason {
3632                crate::engine::OpaqueReason::DynamicReference => 0,
3633                crate::engine::OpaqueReason::RuntimeTextReference => 1,
3634                crate::engine::OpaqueReason::UnknownFunction => 2,
3635                crate::engine::OpaqueReason::UnknownCustomFunction => 3,
3636                crate::engine::OpaqueReason::UnresolvedCrossSheetBinding => 4,
3637                crate::engine::OpaqueReason::UnresolvedName => 5,
3638                crate::engine::OpaqueReason::UnresolvedTable => 6,
3639                crate::engine::OpaqueReason::FormulaName => 7,
3640                crate::engine::OpaqueReason::DeferredSourcePackage => 8,
3641                crate::engine::OpaqueReason::UnsupportedSourceSemantics => 9,
3642                crate::engine::OpaqueReason::UncertainDefaultSheetBinding => 10,
3643            };
3644            1u64 << index
3645        };
3646        if let Some(stats) = self.active_evaluation_resource_request.as_mut() {
3647            stats.staged_selected = report.selected_staged_cells as u64;
3648            stats.staged_retained = report.retained_staged_cells as u64;
3649            stats.target_requested = report.requested_targets as u64;
3650            stats.target_normalized_regions = report.normalized_regions as u64;
3651            stats.target_scope_level = match &report.widened_scope {
3652                crate::engine::PrepareScope::Exact => 0,
3653                crate::engine::PrepareScope::Sheets(_) => 1,
3654                crate::engine::PrepareScope::Workbook => 2,
3655            };
3656            stats.target_widening_reason_bits = report
3657                .widening_reasons
3658                .iter()
3659                .copied()
3660                .fold(0, |bits, reason| bits | reason_bit(reason));
3661            stats.graph_source_scratch_estimated = report.estimated_scratch_bytes;
3662            stats.graph_source_scratch_observed = report.observed_scratch_bytes;
3663            stats.target_commit_estimated_work = report.estimated_commit_work;
3664            stats.target_commit_actual_work = report.actual_commit_work;
3665            stats.target_commit_window_ns = Self::duration_ns(report.commit_window);
3666            stats.phases.staged_prepare_ns = stats
3667                .phases
3668                .staged_prepare_ns
3669                .saturating_add(Self::duration_ns(report.commit_window));
3670        }
3671    }
3672
3673    /// Begin a new evaluation request: reset per-recalc cycle telemetry and
3674    /// take the per-recalc volatile clock sample. Called at the start of
3675    /// every evaluation request that walks schedule units.
3676    fn begin_evaluation_request(&mut self) {
3677        self.freshness_begin_request();
3678        #[cfg(test)]
3679        {
3680            self.evaluation_request_begin_count_for_test = self
3681                .evaluation_request_begin_count_for_test
3682                .saturating_add(1);
3683        }
3684        self.last_cycle_telemetry = CycleTelemetry::default();
3685        self.graph.authority_sync();
3686        // Defensive: consumed at the end of the previous request; a request
3687        // that errored out mid-walk must not leak its members into this one.
3688        self.pending_iterative_redirty.clear();
3689        self.reconcile_retained_sccs_at_request_begin();
3690        // Spec §7.11: NOW()/TODAY() sample the clock ONCE per recalc; every
3691        // read within this request (including SCC iteration passes) observes
3692        // this sample.
3693        self.clock.refresh();
3694    }
3695
3696    /// End-of-recalc redirty: volatile vertices (as always) plus members of
3697    /// SCCs that iterated this recalc without reaching a retainable fixed
3698    /// point (`CyclePolicy::Iterate`), so circular cells re-evaluate on every
3699    /// recalc exactly like Excel's iterative calculation (spec §4
3700    /// persistence / §7.6 accumulator / §7.11 volatile redirty). Retained
3701    /// SCCs (`retained_scc_members`, #368) are left clean. Replaces the bare
3702    /// `graph.redirty_volatiles()` call at every evaluation-flow exit; must
3703    /// run AFTER the flow's `clear_dirty_flags`.
3704    fn redirty_for_next_recalc(&mut self) {
3705        if self.snapshot_evaluation_active {
3706            self.snapshot_volatile_redirty_pending = true;
3707        } else {
3708            self.graph.redirty_volatiles();
3709        }
3710        let pending = std::mem::take(&mut self.pending_iterative_redirty);
3711        let dirty_at_begin = std::mem::take(&mut self.retained_scc_dirty_at_begin);
3712        for (vertex, scc) in dirty_at_begin {
3713            if self.retained_scc_members.get(&vertex) == Some(&scc) {
3714                // No SCC task claimed this member during the request: the
3715                // cycle dissolved and it evaluated as an ordinary formula,
3716                // or the request never reached it. Either way it is no
3717                // longer a retained fixed point. Its persisted value is
3718                // obsolete only if it actually re-evaluated (clean now).
3719                self.retained_scc_members.remove(&vertex);
3720                if !self.graph.is_dirty(vertex) {
3721                    self.iterative_state_values.remove(&vertex);
3722                }
3723            }
3724        }
3725        if !self.iterative_state_values.is_empty() || !self.retained_scc_members.is_empty() {
3726            let graph = &self.graph;
3727            self.iterative_state_values
3728                .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
3729            self.retained_scc_members
3730                .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
3731        }
3732        // Refresh the §4-persistence snapshot for members that re-run each
3733        // recalc: these final values survive structural edits that clear the
3734        // computed overlay (the only value home in canonical mode) so the
3735        // next SCC task can re-seed from them (see `iterative_state_values`).
3736        for &vertex in &pending {
3737            if !self.graph.is_live_formula_vertex(vertex) {
3738                continue;
3739            }
3740            if let Some(cell) = self.graph.get_cell_ref(vertex) {
3741                let sheet_name = self.graph.sheet_name(cell.sheet_id);
3742                match self.get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1) {
3743                    Some(value) if !matches!(value, LiteralValue::Empty) => {
3744                        self.iterative_state_values.insert(vertex, value);
3745                    }
3746                    _ => {
3747                        self.iterative_state_values.remove(&vertex);
3748                    }
3749                }
3750            }
3751        }
3752        if !pending.is_empty() {
3753            self.graph.redirty_iterative_members(&pending);
3754        }
3755    }
3756
3757    /// Hash of every `EvalConfig` knob that can change the result of a
3758    /// retained SCC without any edit reaching the dependency graph. The
3759    /// function registry is tracked separately and precisely (see
3760    /// `retained_scc_function_epoch_seen`).
3761    fn retained_scc_config_fingerprint(&self) -> u64 {
3762        use std::hash::{Hash, Hasher};
3763        let mut hasher = rustc_hash::FxHasher::default();
3764        let config = &self.config;
3765        std::mem::discriminant(&config.cycle.detection).hash(&mut hasher);
3766        match config.cycle.policy {
3767            CyclePolicy::Error => 0u8.hash(&mut hasher),
3768            CyclePolicy::Iterate {
3769                max_iterations,
3770                max_change,
3771            } => {
3772                1u8.hash(&mut hasher);
3773                max_iterations.hash(&mut hasher);
3774                max_change.to_bits().hash(&mut hasher);
3775            }
3776        }
3777        config.date_system.hash(&mut hasher);
3778        config.workbook_seed.hash(&mut hasher);
3779        std::mem::discriminant(&config.volatile_level).hash(&mut hasher);
3780        // `DeterministicMode` carries a timestamp and a timezone spec; hash
3781        // its Debug rendering rather than growing its derive set for this.
3782        format!("{:?}", config.deterministic_mode).hash(&mut hasher);
3783        config.range_expansion_limit.hash(&mut hasher);
3784        config.max_open_ended_rows.hash(&mut hasher);
3785        config.max_open_ended_cols.hash(&mut hasher);
3786        hasher.finish()
3787    }
3788
3789    /// Request-begin bookkeeping for retained SCCs (#368): drop deleted
3790    /// vertices, invalidate everything when the config fingerprint moved
3791    /// (marking retained members dirty so their SCC tasks run in this
3792    /// request), and record how many retained SCCs are being reused, i.e.
3793    /// have no dirty member at request begin.
3794    fn reconcile_retained_sccs_at_request_begin(&mut self) {
3795        self.retained_scc_dirty_at_begin.clear();
3796        if self.retained_scc_members.is_empty() {
3797            return;
3798        }
3799        // Deleted vertices and members overwritten with a literal (the
3800        // vertex survives as a value cell) are no longer retained formulas.
3801        let graph = &self.graph;
3802        self.retained_scc_members
3803            .retain(|vertex, _| graph.is_live_formula_vertex(*vertex));
3804        if self.retained_scc_config_fingerprint() != self.retained_scc_config_fingerprint {
3805            let members: Vec<VertexId> = self.retained_scc_members.keys().copied().collect();
3806            self.retained_scc_members.clear();
3807            self.graph.mark_dirty_many(&members);
3808            return;
3809        }
3810        let changes =
3811            crate::function_registry::semantic_changes_since(self.retained_scc_function_epoch_seen);
3812        let global_changed = changes.epoch != self.retained_scc_function_epoch_seen;
3813        let provider_revision = self.resolver.planning_semantic_revision();
3814        let provider_changed = provider_revision != self.retained_scc_provider_revision_seen;
3815        if global_changed || provider_changed {
3816            let changed: BTreeSet<(String, String)> = changes.keys.into_iter().collect();
3817            let affected: Vec<VertexId> = self
3818                .retained_scc_members
3819                .keys()
3820                .copied()
3821                .filter(|&vertex| {
3822                    let Some(ast) = self.graph.get_formula(vertex) else {
3823                        return true;
3824                    };
3825                    (global_changed
3826                        && (!changes.complete || Self::ast_uses_changed_function(&ast, &changed)))
3827                        || (provider_changed && Self::ast_contains_function(&ast))
3828                })
3829                .collect();
3830            for vertex in &affected {
3831                self.retained_scc_members.remove(vertex);
3832            }
3833            if !affected.is_empty() {
3834                self.graph.mark_dirty_many(&affected);
3835            }
3836            self.retained_scc_function_epoch_seen = changes.epoch;
3837            self.retained_scc_provider_revision_seen = provider_revision;
3838        }
3839        let mut dirty_sccs: FxHashSet<u64> = FxHashSet::default();
3840        let mut all_sccs: FxHashSet<u64> = FxHashSet::default();
3841        for (&vertex, &scc) in &self.retained_scc_members {
3842            all_sccs.insert(scc);
3843            if self.graph.is_dirty(vertex) {
3844                dirty_sccs.insert(scc);
3845                self.retained_scc_dirty_at_begin.push((vertex, scc));
3846            }
3847        }
3848        let reused_members = self
3849            .retained_scc_members
3850            .values()
3851            .filter(|scc| !dirty_sccs.contains(scc))
3852            .count();
3853        let t = &mut self.last_cycle_telemetry;
3854        t.reused_sccs = all_sccs.len() - dirty_sccs.len();
3855        t.reused_scc_members = reused_members;
3856    }
3857
3858    pub fn virtual_dep_fallback_activations(&self) -> u64 {
3859        self.virtual_dep_fallback_activations
3860    }
3861
3862    #[cfg(test)]
3863    pub(crate) fn lookup_index_flights_built_for_test(&self) -> usize {
3864        self.lookup_index_cache
3865            .flights_built
3866            .load(std::sync::atomic::Ordering::Relaxed)
3867    }
3868
3869    pub(crate) fn last_lookup_index_cache_report(&self) -> LookupIndexCacheReport {
3870        self.lookup_index_cache.report()
3871    }
3872
3873    fn lookup_view_contains_volatile(&self, view: &RangeView<'_>, sheet_id: SheetId) -> bool {
3874        let start_row = view.start_row();
3875        let end_row = view.end_row();
3876        let start_col = view.start_col();
3877        let end_col = view.end_col();
3878        for row in start_row..=end_row {
3879            let Ok(row_u32) = u32::try_from(row) else {
3880                return true;
3881            };
3882            for col in start_col..=end_col {
3883                let Ok(col_u32) = u32::try_from(col) else {
3884                    return true;
3885                };
3886                let cell_ref = self
3887                    .graph
3888                    .make_cell_ref_internal(sheet_id, row_u32, col_u32);
3889                if let Some(vertex_id) = self.graph.get_vertex_id_for_address(&cell_ref)
3890                    && self.graph.is_volatile(vertex_id)
3891                {
3892                    return true;
3893                }
3894            }
3895        }
3896        false
3897    }
3898
3899    fn build_lookup_index_impl(
3900        &self,
3901        view: &RangeView<'_>,
3902        axis: LookupAxis,
3903    ) -> Option<Arc<LookupIndex>> {
3904        let (rows, cols) = view.dims();
3905        if rows == 0 || cols == 0 {
3906            self.lookup_index_cache.note_skipped_tiny();
3907            return None;
3908        }
3909        let len = match axis {
3910            LookupAxis::ColumnInView(col) => {
3911                if col >= cols {
3912                    self.lookup_index_cache.note_skipped_tiny();
3913                    return None;
3914                }
3915                rows
3916            }
3917            LookupAxis::RowInView(row) => {
3918                if row >= rows {
3919                    self.lookup_index_cache.note_skipped_tiny();
3920                    return None;
3921                }
3922                cols
3923            }
3924        };
3925        if len < 64 {
3926            self.lookup_index_cache.note_skipped_tiny();
3927            return None;
3928        }
3929
3930        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
3931        let key = LookupIndexKey {
3932            sheet_id,
3933            start_row: u32::try_from(view.start_row()).ok()?,
3934            start_col: u32::try_from(view.start_col()).ok()?,
3935            end_row: u32::try_from(view.end_row()).ok()?,
3936            end_col: u32::try_from(view.end_col()).ok()?,
3937            axis,
3938            snapshot_id: self.data_snapshot_id(),
3939        };
3940        if let Some(index) = self.lookup_index_cache.get(&key) {
3941            return Some(index);
3942        }
3943        if self
3944            .lookup_index_cache
3945            .would_exceed_cap(estimate_bytes(len, 0))
3946        {
3947            self.lookup_index_cache.note_skipped_cap();
3948            return None;
3949        }
3950        if !self.lookup_index_cache.should_build(key) {
3951            return None;
3952        }
3953        // Parallel members of a lookup family miss together: one builds.
3954        self.lookup_index_cache.single_flight(key, || {
3955            if let Some(index) = self.lookup_index_cache.recheck(&key) {
3956                return Some(index);
3957            }
3958            if self.lookup_index_cache.is_known_volatile(&key) {
3959                self.lookup_index_cache.note_skipped_volatile();
3960                return None;
3961            }
3962            if self.lookup_view_contains_volatile(view, sheet_id) {
3963                self.lookup_index_cache.note_volatile_key(key);
3964                self.lookup_index_cache.note_skipped_volatile();
3965                return None;
3966            }
3967            #[cfg(test)]
3968            self.lookup_index_cache
3969                .flights_built
3970                .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
3971            match LookupIndex::build(view, axis, self.config.date_system).ok()? {
3972                BuildOutcome::Built(index) => self.lookup_index_cache.insert_if_room(key, index),
3973                BuildOutcome::ErrorInLookupAxis => {
3974                    self.lookup_index_cache.note_skipped_error();
3975                    None
3976                }
3977                BuildOutcome::Degenerate => {
3978                    self.lookup_index_cache.note_skipped_tiny();
3979                    None
3980                }
3981            }
3982        })
3983    }
3984
3985    fn reset_virtual_dep_telemetry_if_disabled(&mut self) {
3986        if !self.config.enable_virtual_dep_telemetry {
3987            self.last_virtual_dep_telemetry = VirtualDepTelemetry {
3988                fallback_mode_activations: self.virtual_dep_fallback_activations,
3989                ..VirtualDepTelemetry::default()
3990            };
3991        }
3992    }
3993
3994    fn source_cache_session(&self) -> SourceCacheSession {
3995        self.clear_source_cache();
3996        SourceCacheSession {
3997            cache: self.source_cache.clone(),
3998        }
3999    }
4000
4001    fn resolve_source_scalar_cached(
4002        &self,
4003        name: &str,
4004        version: Option<u64>,
4005    ) -> Result<LiteralValue, ExcelError> {
4006        let key = (name.to_string(), version);
4007        if let Ok(mut g) = self.source_cache.write() {
4008            if let Some(v) = g.scalars.get(&key) {
4009                return Ok(v.clone());
4010            }
4011
4012            let v = self.resolver.resolve_source_scalar(name).map_err(|err| {
4013                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4014                    ExcelError::new(ExcelErrorKind::Ref)
4015                        .with_message(format!("Unresolved source scalar: {name}"))
4016                } else {
4017                    err
4018                }
4019            })?;
4020            g.scalars.insert(key, v.clone());
4021            Ok(v)
4022        } else {
4023            self.resolver.resolve_source_scalar(name).map_err(|err| {
4024                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4025                    ExcelError::new(ExcelErrorKind::Ref)
4026                        .with_message(format!("Unresolved source scalar: {name}"))
4027                } else {
4028                    err
4029                }
4030            })
4031        }
4032    }
4033
4034    fn resolve_source_table_cached(
4035        &self,
4036        name: &str,
4037        version: Option<u64>,
4038    ) -> Result<Arc<dyn crate::traits::Table>, ExcelError> {
4039        let key = (name.to_string(), version);
4040        if let Ok(mut g) = self.source_cache.write() {
4041            if let Some(t) = g.tables.get(&key) {
4042                return Ok(t.clone());
4043            }
4044
4045            let t = self.resolver.resolve_source_table(name).map_err(|err| {
4046                if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4047                    ExcelError::new(ExcelErrorKind::Ref)
4048                        .with_message(format!("Unresolved source table: {name}"))
4049                } else {
4050                    err
4051                }
4052            })?;
4053            let t: Arc<dyn crate::traits::Table> = Arc::from(t);
4054            g.tables.insert(key, t.clone());
4055            Ok(t)
4056        } else {
4057            self.resolver
4058                .resolve_source_table(name)
4059                .map_err(|err| {
4060                    if matches!(err.kind, ExcelErrorKind::Name | ExcelErrorKind::NImpl) {
4061                        ExcelError::new(ExcelErrorKind::Ref)
4062                            .with_message(format!("Unresolved source table: {name}"))
4063                    } else {
4064                        err
4065                    }
4066                })
4067                .map(Arc::from)
4068        }
4069    }
4070
4071    fn source_table_to_range_view(
4072        &self,
4073        table: &dyn crate::traits::Table,
4074        spec: &Option<formualizer_parse::parser::TableSpecifier>,
4075    ) -> Result<RangeView<'static>, ExcelError> {
4076        use formualizer_parse::parser::{SpecialItem, TableSpecifier};
4077
4078        let owned = match spec {
4079            Some(TableSpecifier::Column(c)) => {
4080                let c = c.trim();
4081                if c == "@" || c.contains('[') || c.contains(']') || c.contains(',') {
4082                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
4083                        "Complex structured references not yet supported".to_string(),
4084                    ));
4085                }
4086                table.get_column(c)?.materialise().into_owned()
4087            }
4088            Some(TableSpecifier::ColumnRange(start, end)) => {
4089                let cols = table.columns();
4090                let start = start.trim();
4091                let end = end.trim();
4092                let start_key = start.to_lowercase();
4093                let end_key = end.to_lowercase();
4094                let start_idx = cols.iter().position(|n| n.to_lowercase() == start_key);
4095                let end_idx = cols.iter().position(|n| n.to_lowercase() == end_key);
4096                if let (Some(mut si), Some(mut ei)) = (start_idx, end_idx) {
4097                    if si > ei {
4098                        std::mem::swap(&mut si, &mut ei);
4099                    }
4100                    let h = table.data_height();
4101                    let w = ei - si + 1;
4102                    let mut rows = vec![vec![LiteralValue::Empty; w]; h];
4103                    for (offset, ci) in (si..=ei).enumerate() {
4104                        let cname = &cols[ci];
4105                        let col_range = table.get_column(cname)?;
4106                        let (rh, _) = col_range.dimensions();
4107                        for (r, row) in rows.iter_mut().enumerate().take(h.min(rh)) {
4108                            row[offset] = col_range.get(r, 0)?;
4109                        }
4110                    }
4111                    rows
4112                } else {
4113                    return Err(ExcelError::new(ExcelErrorKind::Ref)
4114                        .with_message("Column range refers to unknown column(s)".to_string()));
4115                }
4116            }
4117            Some(TableSpecifier::SpecialItem(SpecialItem::Headers))
4118            | Some(TableSpecifier::Headers) => table
4119                .headers_row()
4120                .map(|r| r.materialise().into_owned())
4121                .unwrap_or_default(),
4122            Some(TableSpecifier::SpecialItem(SpecialItem::Totals))
4123            | Some(TableSpecifier::Totals) => table
4124                .totals_row()
4125                .map(|r| r.materialise().into_owned())
4126                .unwrap_or_default(),
4127            Some(TableSpecifier::SpecialItem(SpecialItem::Data)) | Some(TableSpecifier::Data) => {
4128                table
4129                    .data_body()
4130                    .map(|r| r.materialise().into_owned())
4131                    .unwrap_or_default()
4132            }
4133            Some(TableSpecifier::SpecialItem(SpecialItem::All)) | Some(TableSpecifier::All) => {
4134                let mut out: Vec<Vec<LiteralValue>> = Vec::new();
4135                if let Some(h) = table.headers_row() {
4136                    out.extend(h.iter_rows());
4137                }
4138                if let Some(body) = table.data_body() {
4139                    out.extend(body.iter_rows());
4140                }
4141                if let Some(tr) = table.totals_row() {
4142                    out.extend(tr.iter_rows());
4143                }
4144                out
4145            }
4146            Some(TableSpecifier::SpecialItem(SpecialItem::ThisRow)) => {
4147                return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
4148                    "@ (This Row) requires table-aware context; not yet supported".to_string(),
4149                ));
4150            }
4151            Some(TableSpecifier::Row(_)) | Some(TableSpecifier::Combination(_)) => {
4152                return Err(ExcelError::new(ExcelErrorKind::NImpl)
4153                    .with_message("Complex structured references not yet supported".to_string()));
4154            }
4155            None => {
4156                return Err(ExcelError::new(ExcelErrorKind::NImpl)
4157                    .with_message("Table reference without specifier is unsupported".to_string()));
4158            }
4159        };
4160
4161        Ok(RangeView::from_owned_rows(owned, self.config.date_system))
4162    }
4163
4164    pub fn default_sheet_id(&self) -> SheetId {
4165        self.graph.default_sheet_id()
4166    }
4167
4168    pub fn default_sheet_name(&self) -> &str {
4169        self.graph.default_sheet_name()
4170    }
4171
4172    /// Update the workbook seed for deterministic RNGs in functions.
4173    pub fn set_workbook_seed(&mut self, seed: u64) {
4174        self.config.workbook_seed = seed;
4175    }
4176
4177    /// Set the volatile level policy (Always/OnRecalc/OnOpen)
4178    pub fn set_volatile_level(&mut self, level: crate::traits::VolatileLevel) {
4179        self.config.volatile_level = level;
4180    }
4181
4182    /// Set public temporal materialisation to native values or raw serials.
4183    pub fn set_temporal_egress(&mut self, policy: crate::engine::TemporalEgress) {
4184        self.config.temporal_egress = policy;
4185    }
4186
4187    pub fn temporal_egress(&self) -> crate::engine::TemporalEgress {
4188        self.config.temporal_egress
4189    }
4190
4191    /// Enable/disable deterministic evaluation mode (fixed clock + timezone).
4192    pub fn set_deterministic_mode(
4193        &mut self,
4194        mode: crate::engine::DeterministicMode,
4195    ) -> Result<(), ExcelError> {
4196        let clock = mode.build_clock()?;
4197        self.config.deterministic_mode = mode;
4198        self.clock = crate::timezone::SnapshotClock::new(clock);
4199        Ok(())
4200    }
4201
4202    /// Inject a custom [`ClockProvider`](crate::timezone::ClockProvider) for
4203    /// volatile date/time builtins (`NOW()`, `TODAY()`).
4204    ///
4205    /// The provider is the clock *source*; per spec §7.11 the engine samples
4206    /// it once at the start of every evaluation request and all reads within
4207    /// that recalc (including SCC iteration passes) observe the frozen
4208    /// sample.
4209    pub fn set_clock(&mut self, clock: Arc<dyn crate::timezone::ClockProvider>) {
4210        self.clock = crate::timezone::SnapshotClock::new(clock);
4211    }
4212
4213    fn validate_deterministic_mode(&self) -> Result<(), ExcelError> {
4214        self.config.deterministic_mode.validate()
4215    }
4216
4217    pub fn sheet_id(&self, name: &str) -> Option<SheetId> {
4218        self.graph.sheet_id(name)
4219    }
4220
4221    pub fn sheet_id_mut(&mut self, name: &str) -> SheetId {
4222        self.add_sheet(name)
4223            .unwrap_or_else(|_| self.graph.sheet_id_mut(name))
4224    }
4225
4226    pub fn sheet_name(&self, id: SheetId) -> &str {
4227        self.graph.sheet_name(id)
4228    }
4229
4230    pub fn add_sheet(&mut self, name: &str) -> Result<SheetId, ExcelError> {
4231        let id = self.graph.add_sheet(name)?;
4232        self.ensure_arrow_sheet(name);
4233        self.mark_topology_edited();
4234        Ok(id)
4235    }
4236
4237    pub fn duplicate_sheet(&mut self, source: &str, new_name: &str) -> Result<SheetId, ExcelError> {
4238        let source_id = self.graph.sheet_id(source).ok_or_else(|| {
4239            ExcelError::new(ExcelErrorKind::Value).with_message("Source sheet does not exist")
4240        })?;
4241        if new_name.is_empty() || new_name.len() > 255 {
4242            return Err(ExcelError::new(ExcelErrorKind::Value).with_message("Invalid sheet name"));
4243        }
4244        if self.graph.sheet_id(new_name).is_some() {
4245            return Err(ExcelError::new(ExcelErrorKind::Value)
4246                .with_message(format!("Sheet '{new_name}' already exists")));
4247        }
4248        let new_id = self.graph.duplicate_sheet(source_id, new_name)?;
4249
4250        if let Some(source_sheet) = self.arrow_sheets.sheet(source).cloned() {
4251            let mut copied_sheet = source_sheet;
4252            copied_sheet.name = Arc::<str>::from(new_name);
4253            self.arrow_sheets.sheets.push(copied_sheet);
4254        } else {
4255            self.ensure_arrow_sheet(new_name);
4256        }
4257
4258        let duplicated_formulas = self
4259            .graph
4260            .formula_vertices()
4261            .into_iter()
4262            .filter(|vertex| {
4263                self.graph
4264                    .get_cell_ref(*vertex)
4265                    .is_some_and(|cell| cell.sheet_id == new_id)
4266            })
4267            .collect::<Vec<_>>();
4268        self.graph.mark_vertices_dirty_batch(&duplicated_formulas);
4269        self.mark_topology_edited();
4270        Ok(new_id)
4271    }
4272
4273    fn ensure_arrow_sheet(&mut self, name: &str) {
4274        if self.arrow_sheets.sheet(name).is_some() {
4275            return;
4276        }
4277        self.arrow_sheets
4278            .sheets
4279            .push(crate::arrow_store::ArrowSheet {
4280                name: std::sync::Arc::<str>::from(name),
4281                date_system: self.config.date_system,
4282                columns: Vec::new(),
4283                nrows: 0,
4284                chunk_starts: Vec::new(),
4285                chunk_rows: 32 * 1024,
4286            });
4287    }
4288
4289    pub fn remove_sheet(&mut self, sheet_id: SheetId) -> Result<(), ExcelError> {
4290        let name = self.graph.sheet_name(sheet_id).to_string();
4291        self.purge_derived_formats_for_sheet(sheet_id);
4292        self.graph.remove_sheet(sheet_id)?;
4293        self.arrow_sheets.sheets.retain(|s| s.name.as_ref() != name);
4294        // Sheet removal can change cross-sheet refs, names, and default-sheet
4295        // resolution. Until those domains have a complete exact dependency
4296        // proof, retain the documented graph-owned global invalidation.
4297        self.clear_all_computed_overlays();
4298        self.mark_all_formula_vertices_dirty();
4299        self.clear_staged_formulas_for_sheet(&name);
4300        if self.row_visibility.remove(&sheet_id).is_some() {
4301            self.invalidate_row_visibility_mask_cache();
4302        }
4303        self.record_structural_change(StructuralScope::RemovedSheet(sheet_id));
4304        self.mark_topology_edited();
4305        Ok(())
4306    }
4307
4308    /// Helper to synchronize the Arrow-backed storage layer.
4309    fn rename_sheet_in_arrow_store(&mut self, target_name: &str, new_name: &str) -> bool {
4310        if let Some(asheet) = self
4311            .arrow_sheets
4312            .sheets
4313            .iter_mut()
4314            .find(|s| s.name.as_ref() == target_name)
4315        {
4316            asheet.name = std::sync::Arc::<str>::from(new_name);
4317            return true;
4318        }
4319        false
4320    }
4321
4322    pub fn rename_sheet(&mut self, sheet_id: SheetId, new_name: &str) -> Result<(), ExcelError> {
4323        let old_name = self.graph.sheet_name(sheet_id).to_string();
4324
4325        // Speculative Storage Update
4326        // Update name in storage FIRST so the Evaluator can find it during Graph rescue.
4327        self.rename_sheet_in_arrow_store(&old_name, new_name);
4328
4329        // Graph Update (Metadata + Rescue Logic)
4330        match self.graph.rename_sheet(sheet_id, new_name) {
4331            Ok(_) => {
4332                self.rename_staged_formula_sheet(&old_name, new_name);
4333                // Success! Invalidate cache for the moved sheet
4334                let sheet_vertices: Vec<VertexId> = self
4335                    .graph
4336                    .grid_vertices_in_sheet(sheet_id)
4337                    .map(|(id, _)| id)
4338                    .collect();
4339                for v_id in sheet_vertices {
4340                    self.graph.mark_vertex_dirty(v_id);
4341                }
4342                // Sheet rename preserves SheetId and therefore formula dependencies.
4343                self.mark_topology_edited();
4344                Ok(())
4345            }
4346            Err(e) => {
4347                // ROLLBACK: Revert storage if graph rejected the name
4348                self.rename_sheet_in_arrow_store(new_name, &old_name);
4349                Err(e)
4350            }
4351        }
4352    }
4353
4354    pub fn named_ranges_iter(
4355        &self,
4356    ) -> impl Iterator<Item = (&String, &crate::engine::named_range::NamedRange)> {
4357        self.graph.named_ranges_iter()
4358    }
4359
4360    pub fn sheet_named_ranges_iter(
4361        &self,
4362    ) -> impl Iterator<Item = (&(SheetId, String), &crate::engine::named_range::NamedRange)> {
4363        self.graph.sheet_named_ranges_iter()
4364    }
4365
4366    pub fn resolve_name_entry(
4367        &self,
4368        name: &str,
4369        current_sheet: SheetId,
4370    ) -> Option<&crate::engine::named_range::NamedRange> {
4371        self.graph.resolve_name_entry(name, current_sheet)
4372    }
4373
4374    /// The [`NameScope`] an optional scope-sheet argument denotes.
4375    ///
4376    /// `None` means **workbook scope**, not "the default sheet": a caller that
4377    /// supplies no sheet context is asking about workbook-scoped names only.
4378    /// An unknown sheet name is a malformed query and errors rather than
4379    /// silently degrading to another sheet's scope (issue #110).
4380    ///
4381    /// This is the one owned derivation from `Option<&str>` to a name scope;
4382    /// every scope-taking entry point routes through it.
4383    pub(crate) fn name_query_scope(
4384        &self,
4385        scope_sheet: Option<&str>,
4386    ) -> Result<NameScope, ExcelError> {
4387        match scope_sheet {
4388            None => Ok(NameScope::Workbook),
4389            Some(sheet) => self
4390                .graph
4391                .sheet_id(sheet)
4392                .map(NameScope::Sheet)
4393                .ok_or_else(|| {
4394                    ExcelError::new(ExcelErrorKind::Ref)
4395                        .with_message(format!("name scope sheet not found: {sheet}"))
4396                }),
4397        }
4398    }
4399
4400    /// Whether `name` resolves in the scope denoted by `scope_sheet`.
4401    ///
4402    /// `scope_sheet == None` asks about workbook scope only; a name scoped to a
4403    /// single sheet (including the default sheet) does not answer it. An unknown
4404    /// sheet name resolves nothing.
4405    /// Resolve a [`SharedSheetLocator`](crate::reference::SharedSheetLocator)
4406    /// against an explicit context sheet.
4407    ///
4408    /// Thin forwarder to
4409    /// [`SheetRegistry::resolve_locator`](crate::engine::sheet_registry::SheetRegistry::resolve_locator),
4410    /// the single owned derivation. `Current` resolves to `context_sheet`, never
4411    /// to the workbook's default sheet.
4412    fn resolve_sheet_locator(
4413        &self,
4414        locator: &crate::reference::SharedSheetLocator<'_>,
4415        context_sheet: SheetId,
4416    ) -> Result<SheetId, ExcelError> {
4417        self.graph
4418            .sheet_reg()
4419            .resolve_locator(locator, context_sheet)
4420    }
4421
4422    pub fn has_name(&self, name: &str, scope_sheet: Option<&str>) -> bool {
4423        let Ok(scope) = self.name_query_scope(scope_sheet) else {
4424            return false;
4425        };
4426        self.graph
4427            .resolve_name_entry_in_scope(name, scope)
4428            .is_some()
4429    }
4430
4431    /// The current value of `name` in the scope denoted by `scope_sheet`.
4432    ///
4433    /// Scoping follows [`Self::has_name`]: `None` is workbook scope only.
4434    pub fn resolved_name_value(
4435        &self,
4436        name: &str,
4437        scope_sheet: Option<&str>,
4438    ) -> Option<LiteralValue> {
4439        let scope = self.name_query_scope(scope_sheet).ok()?;
4440        let entry = self.graph.resolve_name_entry_in_scope(name, scope)?;
4441        self.graph.get_value(entry.vertex)
4442    }
4443
4444    pub fn table_metadata(&self, name: &str) -> Option<TableMetadata> {
4445        let entry = self.graph.resolve_table_entry(name)?;
4446        Some(TableMetadata {
4447            name: entry.name.clone(),
4448            sheet: self.graph.sheet_name(entry.sheet_id()).to_string(),
4449            start_row: entry.range.start.coord.row() + 1,
4450            start_col: entry.range.start.coord.col() + 1,
4451            end_row: entry.range.end.coord.row() + 1,
4452            end_col: entry.range.end.coord.col() + 1,
4453            header_row: entry.header_row,
4454            headers: entry.headers.clone(),
4455            totals_row: entry.totals_row,
4456        })
4457    }
4458
4459    /// Metadata for every defined table, ordered by name.
4460    pub fn tables(&self) -> Vec<TableMetadata> {
4461        self.graph
4462            .table_names()
4463            .into_iter()
4464            .filter_map(|name| self.table_metadata(&name))
4465            .collect()
4466    }
4467
4468    pub fn named_ranges_snapshot(&self) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
4469        let mut out: Vec<crate::engine::named_range::NamedRangeSnapshot> = Vec::new();
4470
4471        for (name, named) in self.graph.named_ranges_iter() {
4472            out.push(crate::engine::named_range::NamedRangeSnapshot {
4473                name: name.clone(),
4474                scope: NameScope::Workbook,
4475                definition: named.definition.clone(),
4476            });
4477        }
4478
4479        for ((sheet_id, name), named) in self.graph.sheet_named_ranges_iter() {
4480            out.push(crate::engine::named_range::NamedRangeSnapshot {
4481                name: name.clone(),
4482                scope: NameScope::Sheet(*sheet_id),
4483                definition: named.definition.clone(),
4484            });
4485        }
4486
4487        out.sort_by(|a, b| {
4488            let a_scope = match a.scope {
4489                NameScope::Workbook => (0u8, 0u32),
4490                NameScope::Sheet(id) => (1u8, u32::from(id)),
4491            };
4492            let b_scope = match b.scope {
4493                NameScope::Workbook => (0u8, 0u32),
4494                NameScope::Sheet(id) => (1u8, u32::from(id)),
4495            };
4496            a_scope.cmp(&b_scope).then_with(|| a.name.cmp(&b.name))
4497        });
4498
4499        out
4500    }
4501
4502    pub fn named_ranges_snapshot_for_sheet(
4503        &self,
4504        sheet_id: SheetId,
4505    ) -> Vec<crate::engine::named_range::NamedRangeSnapshot> {
4506        self.named_ranges_snapshot()
4507            .into_iter()
4508            .filter(|entry| match entry.scope {
4509                NameScope::Workbook => true,
4510                NameScope::Sheet(id) => id == sheet_id,
4511            })
4512            .collect()
4513    }
4514
4515    pub fn define_name(
4516        &mut self,
4517        name: &str,
4518        definition: NamedDefinition,
4519        scope: NameScope,
4520    ) -> Result<(), ExcelError> {
4521        self.graph.validate_define_name(name, scope)?;
4522        self.graph.define_name(name, definition, scope)?;
4523        self.record_structural_change(StructuralScope::AllSheets);
4524
4525        self.mark_topology_edited();
4526
4527        Ok(())
4528    }
4529
4530    pub fn update_name(
4531        &mut self,
4532        name: &str,
4533        definition: NamedDefinition,
4534        scope: NameScope,
4535    ) -> Result<(), ExcelError> {
4536        self.graph.validate_existing_name(name, scope)?;
4537        self.graph.update_name(name, definition, scope)?;
4538        self.record_structural_change(StructuralScope::AllSheets);
4539
4540        self.mark_topology_edited();
4541
4542        Ok(())
4543    }
4544
4545    pub fn delete_name(&mut self, name: &str, scope: NameScope) -> Result<(), ExcelError> {
4546        self.graph.validate_existing_name(name, scope)?;
4547        self.graph.delete_name(name, scope)?;
4548        self.record_structural_change(StructuralScope::AllSheets);
4549
4550        self.mark_topology_edited();
4551
4552        Ok(())
4553    }
4554
4555    pub fn define_table(
4556        &mut self,
4557        name: &str,
4558        range: crate::reference::RangeRef,
4559        header_row: bool,
4560        headers: Vec<String>,
4561        totals_row: bool,
4562    ) -> Result<(), ExcelError> {
4563        self.graph
4564            .define_table(name, range, header_row, headers, totals_row)?;
4565        self.record_structural_change(StructuralScope::AllSheets);
4566        self.mark_topology_edited();
4567        Ok(())
4568    }
4569
4570    pub fn define_source_scalar(
4571        &mut self,
4572        name: &str,
4573        version: Option<u64>,
4574    ) -> Result<(), ExcelError> {
4575        self.graph.define_source_scalar(name, version)?;
4576        self.record_structural_change(StructuralScope::OpaqueGlobal);
4577        self.mark_topology_edited();
4578        Ok(())
4579    }
4580
4581    pub fn define_source_table(
4582        &mut self,
4583        name: &str,
4584        version: Option<u64>,
4585    ) -> Result<(), ExcelError> {
4586        self.graph.define_source_table(name, version)?;
4587        self.record_structural_change(StructuralScope::OpaqueGlobal);
4588        self.mark_topology_edited();
4589        Ok(())
4590    }
4591
4592    pub fn set_source_scalar_version(
4593        &mut self,
4594        name: &str,
4595        version: Option<u64>,
4596    ) -> Result<(), ExcelError> {
4597        self.graph.set_source_scalar_version(name, version)?;
4598        Ok(())
4599    }
4600
4601    pub fn set_source_table_version(
4602        &mut self,
4603        name: &str,
4604        version: Option<u64>,
4605    ) -> Result<(), ExcelError> {
4606        self.graph.set_source_table_version(name, version)?;
4607        Ok(())
4608    }
4609
4610    pub fn invalidate_source(&mut self, name: &str) -> Result<(), ExcelError> {
4611        self.graph.invalidate_source(name)?;
4612        Ok(())
4613    }
4614
4615    pub fn vertex_value(&self, vertex: VertexId) -> Option<LiteralValue> {
4616        self.graph.get_value(vertex)
4617    }
4618
4619    pub fn graph_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
4620        self.graph.get_cell_value(sheet, row, col)
4621    }
4622
4623    pub fn vertex_for_cell(&self, cell: &CellRef) -> Option<VertexId> {
4624        self.graph.get_vertex_for_cell(cell)
4625    }
4626
4627    pub fn evaluation_vertices(&self) -> Vec<VertexId> {
4628        self.graph.get_evaluation_vertices()
4629    }
4630
4631    /// Return read-only baseline counters for dispatch benchmarking.
4632    pub fn baseline_stats(&self) -> EngineBaselineStats {
4633        let graph = self.graph.baseline_stats();
4634        EngineBaselineStats {
4635            graph_vertex_count: graph.graph_vertex_count,
4636            graph_formula_vertex_count: graph.graph_formula_vertex_count,
4637            graph_edge_count: graph.graph_edge_count,
4638            dirty_vertex_count: graph.dirty_vertex_count,
4639            evaluation_vertex_count: graph.evaluation_vertex_count,
4640            formula_ast_root_count: graph.formula_ast_root_count,
4641            formula_ast_node_count: graph.formula_ast_node_count,
4642            staged_formula_count: self.staged_formula_count(),
4643            formula_plane_active_span_count: 0,
4644            formula_plane_producer_result_entries: 0,
4645            formula_plane_consumer_read_entries: 0,
4646            formula_plane_mixed_topology_cache_builds: 0,
4647            formula_plane_mixed_topology_cache_hits: 0,
4648            formula_plane_mixed_topology_cache_overflows: 0,
4649            formula_plane_dirty_pending_events: 0,
4650            formula_plane_dirty_region_events_recorded: 0,
4651            formula_plane_dirty_span_region_events_recorded: 0,
4652            formula_plane_dirty_whole_span_seeds_recorded: 0,
4653            formula_plane_dirty_global_invalidations: 0,
4654            formula_plane_structural_span_candidates: 0,
4655            formula_plane_cycle_member_span_demotions: 0,
4656            formula_plane_array_result_span_demotions: 0,
4657            retained_scc_members: self.retained_scc_members.len(),
4658        }
4659    }
4660
4661    /// Mutation revision captured by read-only engine reports.
4662    pub(crate) fn inspection_mutation_revision(&self) -> u64 {
4663        self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
4664    }
4665
4666    #[cfg(test)]
4667    pub(crate) fn used_axis_bounds_cache_stats(&self) -> (usize, usize, usize, usize) {
4668        self.used_axis_bounds_cache
4669            .read()
4670            .ok()
4671            .and_then(|guard| {
4672                guard.as_ref().map(|cache| {
4673                    (
4674                        cache.row_hits.load(Ordering::Relaxed),
4675                        cache.row_misses.load(Ordering::Relaxed),
4676                        cache.col_hits.load(Ordering::Relaxed),
4677                        cache.col_misses.load(Ordering::Relaxed),
4678                    )
4679                })
4680            })
4681            .unwrap_or((0, 0, 0, 0))
4682    }
4683
4684    pub fn set_first_load_assume_new(&mut self, enabled: bool) {
4685        self.graph.set_first_load_assume_new(enabled);
4686    }
4687
4688    pub fn first_load_assume_new(&self) -> bool {
4689        self.graph.first_load_assume_new()
4690    }
4691
4692    pub fn reset_ensure_touched(&mut self) {
4693        self.graph.reset_ensure_touched();
4694    }
4695
4696    pub fn finalize_sheet_index(&mut self, sheet: &str) {
4697        self.graph.finalize_sheet_index(sheet);
4698    }
4699
4700    /// Execute a named Engine action.
4701    ///
4702    /// Ticket 614 introduces this as the stable Engine-level transaction surface.
4703    /// For now actions are commit-only: they do not create changelog boundaries and they do not
4704    /// provide rollback/atomicity.
4705    ///
4706    /// Nested actions are deterministically handled by *disallowing* nesting: calling
4707    /// `Engine::action` while another action is active returns `EditorError::TransactionFailed`.
4708    pub fn action<T>(
4709        &mut self,
4710        name: impl AsRef<str>,
4711        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4712    ) -> Result<T, crate::engine::EditorError> {
4713        if self.action_depth != 0 {
4714            return Err(crate::engine::EditorError::TransactionFailed {
4715                reason: "Nested Engine::action calls are not supported (ticket 614: commit-only surface)"
4716                    .to_string(),
4717            });
4718        }
4719
4720        self.action_depth = 1;
4721        let engine_ptr: *mut Engine<R> = self;
4722        let _guard = ActionDepthGuard {
4723            engine: engine_ptr,
4724            _marker: std::marker::PhantomData,
4725        };
4726
4727        let mut tx = EngineAction {
4728            engine: self,
4729            name: name.as_ref().to_string(),
4730            capture: None,
4731            arrow_undo: None,
4732            atomic_policy: false,
4733        };
4734        f(&mut tx)
4735    }
4736
4737    /// Execute a named Engine action with atomic commit/rollback semantics.
4738    ///
4739    /// This variant does not require a `ChangeLog` and uses an internal journal for rollback.
4740    pub fn action_atomic<T>(
4741        &mut self,
4742        name: impl Into<String>,
4743        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4744    ) -> Result<T, crate::engine::EditorError> {
4745        let (v, _j) = self.action_atomic_journal(name, f)?;
4746        Ok(v)
4747    }
4748
4749    /// Like `action_atomic`, but returns the committed journal entry for undo/redo storage.
4750    pub fn action_atomic_journal<T>(
4751        &mut self,
4752        name: impl Into<String>,
4753        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4754    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
4755        if self.action_depth != 0 {
4756            return Err(crate::engine::EditorError::TransactionFailed {
4757                reason: "Nested Engine::action calls are not supported (deterministic rule)"
4758                    .to_string(),
4759            });
4760        }
4761
4762        self.action_depth = 1;
4763        let engine_ptr: *mut Engine<R> = self;
4764        let _guard = ActionDepthGuard {
4765            engine: engine_ptr,
4766            _marker: std::marker::PhantomData,
4767        };
4768
4769        let name_str = name.into();
4770        let mut capture = MutationCapture::new(Default::default());
4771        let start_len = capture.len();
4772        self.action_atomic_impl(&mut capture, start_len, true, name_str, f)
4773    }
4774
4775    fn action_atomic_impl<T>(
4776        &mut self,
4777        capture: &mut MutationCapture,
4778        start_len: usize,
4779        expand_runs: bool,
4780        name: String,
4781        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4782    ) -> Result<(T, crate::engine::ActionJournal), crate::engine::EditorError> {
4783        let invalidation_baseline = self.invalidation_baseline();
4784        let mut arrow_undo = crate::engine::ArrowUndoBatch::default();
4785        let arrow_ptr: *mut crate::engine::ArrowUndoBatch = &mut arrow_undo;
4786
4787        let capture_ptr: *mut MutationCapture = capture;
4788        let mut tx = EngineAction {
4789            engine: self,
4790            name: name.clone(),
4791            capture: Some(capture_ptr),
4792            arrow_undo: Some(arrow_ptr),
4793            atomic_policy: true,
4794        };
4795
4796        let res = f(&mut tx);
4797
4798        // Capture graph structural delta for this action. The journal is a
4799        // public value: run records (Program 2) are expanded into it, except
4800        // for a caller that only publishes the capture to its change log
4801        // (`expand_runs` false), where the journal (plain events) drives
4802        // invalidation and records count as topology changes; a rollback
4803        // still replays the expanded events.
4804        let capture_ref = unsafe { &*capture_ptr };
4805        let has_runs = capture_ref.lazy_len() > 0;
4806        let graph_events: Vec<crate::engine::ChangeEvent> = if expand_runs || res.is_err() {
4807            capture_ref.expanded_events_from(start_len, 0)
4808        } else {
4809            capture_ref.events()[start_len..].to_vec()
4810        };
4811        let graph_batch = crate::engine::GraphUndoBatch {
4812            events: graph_events,
4813        };
4814        let affected_cells = arrow_undo.ops.len();
4815        let journal = crate::engine::ActionJournal {
4816            name,
4817            graph: graph_batch,
4818            arrow: arrow_undo,
4819            affected_cells,
4820        };
4821
4822        match res {
4823            Ok(v) => {
4824                if !journal.graph.is_empty() || !journal.arrow.is_empty() || has_runs {
4825                    for event in &journal.graph.events {
4826                        self.record_change_for_event(event);
4827                    }
4828                    let mut impact = Self::classify_change_events(
4829                        &journal.graph.events,
4830                        LoggedEditDirection::Original,
4831                    )
4832                    .max(Self::classify_arrow_undo(&journal.arrow));
4833                    if has_runs {
4834                        impact = impact.max(LoggedEditImpact::Topology);
4835                    }
4836                    self.apply_logged_edit_impact(impact, invalidation_baseline);
4837                }
4838                Ok((v, journal))
4839            }
4840            Err(e) => {
4841                if let Err(rb) = self.rollback_from_action_journal(&journal, invalidation_baseline)
4842                {
4843                    return Err(crate::engine::EditorError::TransactionFailed {
4844                        reason: format!(
4845                            "Engine::action_atomic rollback failed after error '{e}': {rb}"
4846                        ),
4847                    });
4848                }
4849                if !journal.graph.is_empty() || !journal.arrow.is_empty() {
4850                    for event in &journal.graph.events {
4851                        self.record_change_for_event(event);
4852                    }
4853                }
4854                Err(e)
4855            }
4856        }
4857    }
4858
4859    /// Execute a named Engine action, logging graph changes into the provided ChangeLog.
4860    ///
4861    /// Ticket 615: this variant provides atomicity. If the action returns an error, it rolls back:
4862    /// - Dependency graph structural edits (via inverse ChangeEvents)
4863    /// - Arrow-truth overlay writes mirrored from ChangeEvents
4864    /// - ChangeLog entries (published only after a successful commit)
4865    pub fn action_with_logger<T>(
4866        &mut self,
4867        log: &mut crate::engine::ChangeLog,
4868        name: impl AsRef<str>,
4869        f: impl FnOnce(&mut EngineAction<'_, R>) -> Result<T, crate::engine::EditorError>,
4870    ) -> Result<T, crate::engine::EditorError> {
4871        if self.action_depth != 0 {
4872            return Err(crate::engine::EditorError::TransactionFailed {
4873                reason: "Nested Engine::action calls are not supported (deterministic rule)"
4874                    .to_string(),
4875            });
4876        }
4877
4878        self.action_depth = 1;
4879        let engine_ptr: *mut Engine<R> = self;
4880        let _guard = ActionDepthGuard {
4881            engine: engine_ptr,
4882            _marker: std::marker::PhantomData,
4883        };
4884
4885        let name_str = name.as_ref().to_string();
4886        let mut capture = MutationCapture::new(log.current_meta());
4887        let start_len = capture.len();
4888        capture.begin_compound(name_str.clone());
4889
4890        // Mutation correctness uses the complete private capture. The provided ChangeLog remains
4891        // an observability sink and is not touched until the action outcome is known.
4892        let res = self.action_atomic_impl(&mut capture, start_len, false, name_str, f);
4893        capture.close_compounds();
4894
4895        match res {
4896            Ok((v, _journal)) => {
4897                log.publish_capture(capture);
4898                Ok(v)
4899            }
4900            Err(e) => {
4901                // Preserve sequence/group gaps without retaining failed events or evicting history.
4902                log.discard_capture(capture);
4903                Err(e)
4904            }
4905        }
4906    }
4907
4908    fn rollback_from_action_journal(
4909        &mut self,
4910        journal: &crate::engine::ActionJournal,
4911        invalidation_baseline: InvalidationBaseline,
4912    ) -> Result<(), crate::engine::EditorError> {
4913        // Invalidate first so a partial inverse failure cannot leave a changed
4914        // graph behind an apparently current schedule or lookup cache.
4915        self.invalidate_for_action_journal(
4916            journal,
4917            LoggedEditDirection::InverseReplay,
4918            invalidation_baseline,
4919        );
4920        // 1) Roll back the dependency graph structure.
4921        journal.graph.undo(&mut self.graph)?;
4922        // 2) Roll back engine row-visibility sidecar events.
4923        self.apply_inverse_row_visibility_events(&journal.graph.events);
4924        // 3) Roll back Arrow-truth overlays.
4925        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
4926        Ok(())
4927    }
4928
4929    fn rollback_from_change_events(
4930        &mut self,
4931        events: &[crate::engine::ChangeEvent],
4932        invalidation_baseline: InvalidationBaseline,
4933    ) -> Result<(), crate::engine::EditorError> {
4934        use crate::engine::ChangeEvent;
4935
4936        // Fail closed before applying inverses because replay can return after
4937        // only part of the batch has been restored.
4938        self.invalidate_for_change_events(
4939            events,
4940            LoggedEditDirection::InverseReplay,
4941            invalidation_baseline,
4942        );
4943
4944        // 1) Roll back the dependency graph.
4945        self.graph
4946            .authority_set_replay(crate::engine::authority::history::Replay::Undo);
4947        let rolled_back = (|| {
4948            let mut editor = crate::engine::VertexEditor::new(&mut self.graph);
4949            let mut compound_stack: Vec<usize> = Vec::new();
4950            for (i, ev) in events.iter().enumerate().rev() {
4951                match ev {
4952                    ChangeEvent::CompoundEnd { depth } => {
4953                        compound_stack.push(*depth);
4954                        if let Some(description) =
4955                            crate::engine::graph::editor::change_log::compound_start_description(
4956                                i,
4957                                |j| &events[j],
4958                            )
4959                        {
4960                            editor.inverse_compound_end(description);
4961                        }
4962                    }
4963                    ChangeEvent::CompoundStart { depth, .. } => {
4964                        if compound_stack.last() == Some(depth) {
4965                            compound_stack.pop();
4966                        }
4967                        editor.apply_inverse(ev.clone())?;
4968                    }
4969                    ChangeEvent::SetRowVisibility { .. } => {
4970                        // Engine-side metadata handled after dropping graph editor borrow.
4971                    }
4972                    _ => {
4973                        editor.apply_inverse(ev.clone())?;
4974                    }
4975                }
4976            }
4977            Ok::<_, crate::engine::EditorError>(())
4978        })();
4979        self.graph
4980            .authority_set_replay(crate::engine::authority::history::Replay::Forward);
4981        rolled_back?;
4982
4983        // 2) Roll back engine row-visibility metadata.
4984        for ev in events.iter().rev() {
4985            self.apply_inverse_row_visibility_event(ev);
4986        }
4987
4988        // 3) Roll back Arrow-truth overlays mirrored from those ChangeEvents.
4989        for ev in events.iter().rev() {
4990            self.mirror_inverse_change_to_arrow(ev);
4991        }
4992
4993        Ok(())
4994    }
4995
4996    fn read_cell_formula_ast(&self, sheet: &str, row: u32, col: u32) -> Option<ASTNode> {
4997        let sheet_id = self.graph.sheet_id(sheet)?;
4998        let coord = Coord::from_excel(row, col, true, true);
4999        let cell = CellRef::new(sheet_id, coord);
5000        let vid = self.graph.get_vertex_for_cell(&cell)?;
5001        self.graph.get_formula(vid)
5002    }
5003
5004    pub fn define_name_with_logger(
5005        &mut self,
5006        log: &mut crate::engine::ChangeLog,
5007        name: &str,
5008        definition: NamedDefinition,
5009        scope: NameScope,
5010    ) -> Result<(), crate::engine::EditorError> {
5011        self.graph
5012            .validate_define_name(name, scope)
5013            .map_err(crate::engine::EditorError::Excel)?;
5014
5015        {
5016            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
5017            editor.define_name(name, definition, scope)?;
5018        }
5019        self.record_structural_change(StructuralScope::AllSheets);
5020
5021        self.mark_topology_edited();
5022
5023        Ok(())
5024    }
5025
5026    pub fn update_name_with_logger(
5027        &mut self,
5028        log: &mut crate::engine::ChangeLog,
5029        name: &str,
5030        definition: NamedDefinition,
5031        scope: NameScope,
5032    ) -> Result<(), crate::engine::EditorError> {
5033        self.graph
5034            .validate_existing_name(name, scope)
5035            .map_err(crate::engine::EditorError::Excel)?;
5036        {
5037            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
5038            editor.update_name(name, definition, scope)?;
5039        }
5040        self.record_structural_change(StructuralScope::AllSheets);
5041
5042        self.mark_topology_edited();
5043
5044        Ok(())
5045    }
5046
5047    pub fn delete_name_with_logger(
5048        &mut self,
5049        log: &mut crate::engine::ChangeLog,
5050        name: &str,
5051        scope: NameScope,
5052    ) -> Result<(), crate::engine::EditorError> {
5053        self.graph
5054            .validate_existing_name(name, scope)
5055            .map_err(crate::engine::EditorError::Excel)?;
5056        {
5057            let mut editor = crate::engine::VertexEditor::with_logger(&mut self.graph, log);
5058            editor.delete_name(name, scope)?;
5059        }
5060        self.record_structural_change(StructuralScope::AllSheets);
5061
5062        self.mark_topology_edited();
5063
5064        Ok(())
5065    }
5066
5067    pub fn edit_with_logger<T>(
5068        &mut self,
5069        log: &mut crate::engine::ChangeLog,
5070        f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
5071    ) -> Result<T, crate::engine::EditorError> {
5072        let mut capture = MutationCapture::new(log.current_meta());
5073        let result = self.edit_with_capture(&mut capture, f);
5074        capture.close_compounds();
5075        match result {
5076            Ok(value) => {
5077                log.publish_capture(capture);
5078                Ok(value)
5079            }
5080            Err(error) => {
5081                log.discard_capture(capture);
5082                Err(error)
5083            }
5084        }
5085    }
5086
5087    fn edit_with_capture<T>(
5088        &mut self,
5089        capture: &mut MutationCapture,
5090        f: impl FnOnce(&mut crate::engine::VertexEditor) -> T,
5091    ) -> Result<T, crate::engine::EditorError> {
5092        let invalidation_baseline = self.invalidation_baseline();
5093        let start_len = capture.len();
5094        let lazy_start = capture.lazy_len();
5095
5096        // Provide a spill snapshot reader so VertexEditor can snapshot Arrow-truth spill values
5097        // (graph value cache is intentionally empty in canonical mode).
5098        struct ArrowSpillReader<'a> {
5099            sheets: &'a crate::arrow_store::SheetStore,
5100        }
5101        impl crate::engine::graph::editor::vertex_editor::SpillValueReader for ArrowSpillReader<'_> {
5102            fn read_cell_value(
5103                &self,
5104                sheet: &str,
5105                row: u32,
5106                col: u32,
5107            ) -> Option<formualizer_common::LiteralValue> {
5108                use formualizer_common::LiteralValue;
5109                let asheet = self.sheets.sheet(sheet)?;
5110                let r0 = row.saturating_sub(1) as usize;
5111                let c0 = col.saturating_sub(1) as usize;
5112                let v = asheet.get_cell_value(r0, c0);
5113                if matches!(v, LiteralValue::Empty) {
5114                    None
5115                } else {
5116                    Some(v)
5117                }
5118            }
5119        }
5120
5121        let ret = {
5122            let spill_reader = ArrowSpillReader {
5123                sheets: &self.arrow_sheets,
5124            };
5125            let mut editor = crate::engine::VertexEditor::with_capture_and_spill_reader(
5126                &mut self.graph,
5127                capture,
5128                &spill_reader,
5129            );
5130            f(&mut editor)
5131        };
5132
5133        // Plain events only: run records (Program 2) stand for
5134        // `FormulaAdjusted` events, which have no forward effect here but
5135        // topology invalidation.
5136        let new_events = capture.events()[start_len..].to_vec();
5137        let new_runs = capture.lazy_len() > lazy_start;
5138        if new_events.iter().any(|event| {
5139            matches!(
5140                event,
5141                ChangeEvent::DefineName { .. }
5142                    | ChangeEvent::UpdateName { .. }
5143                    | ChangeEvent::DeleteName { .. }
5144            )
5145        }) {
5146            let all = capture.expanded_events_from(start_len, lazy_start);
5147            self.rollback_from_change_events(&all, invalidation_baseline)?;
5148            return Err(crate::engine::EditorError::TransactionUnsupported {
5149                reason: "name mutations must use Engine's prepared logged-name APIs".to_string(),
5150            });
5151        }
5152
5153        // Mirror value-impacting graph events to Arrow for forward edits.
5154        // This keeps Arrow overlays (delta + computed) consistent when edits clear/commit spills.
5155        self.clear_logged_cell_format_states(&new_events);
5156        for ev in &new_events {
5157            self.mirror_forward_change_to_arrow(ev);
5158        }
5159        for ev in &new_events {
5160            self.record_change_for_event(ev);
5161        }
5162
5163        // Atomic EngineAction calls publish one invalidation for their complete
5164        // journal at commit/rollback. Direct logged edits publish here.
5165        if self.action_depth == 0 {
5166            let mut impact =
5167                Self::classify_change_events(&new_events, LoggedEditDirection::Original);
5168            if new_runs {
5169                impact = impact.max(LoggedEditImpact::Topology);
5170            }
5171            self.apply_logged_edit_impact(impact, invalidation_baseline);
5172        }
5173
5174        Ok(ret)
5175    }
5176
5177    pub(crate) fn preflight_replay_admission(
5178        &mut self,
5179        events: &[ChangeEvent],
5180        forward: bool,
5181    ) -> Result<(), crate::engine::EditorError> {
5182        if !self.graph_admission_enabled() {
5183            return Ok(());
5184        }
5185        let mut vertex_delta = 0i128;
5186        let mut edge_delta = 0i128;
5187        let mut added_vertices = 0usize;
5188        let mut added_edges = 0usize;
5189        let mut formula_cells = BTreeSet::new();
5190        for event in events {
5191            match event {
5192                ChangeEvent::AddVertex {
5193                    formula,
5194                    coord,
5195                    sheet_id,
5196                    ..
5197                } => {
5198                    let delta = if forward { 1 } else { -1 };
5199                    vertex_delta += delta;
5200                    if forward {
5201                        added_vertices = added_vertices.saturating_add(1);
5202                        if formula.is_some() {
5203                            formula_cells.insert((*sheet_id, coord.row(), coord.col()));
5204                        }
5205                    }
5206                }
5207                ChangeEvent::RemoveVertex {
5208                    old_formula,
5209                    coord,
5210                    sheet_id,
5211                    ..
5212                } => {
5213                    let delta = if forward { -1 } else { 1 };
5214                    vertex_delta += delta;
5215                    if !forward {
5216                        added_vertices = added_vertices.saturating_add(1);
5217                        if old_formula.is_some()
5218                            && let (Some(sheet_id), Some(coord)) = (sheet_id, coord)
5219                        {
5220                            formula_cells.insert((*sheet_id, coord.row(), coord.col()));
5221                        }
5222                    }
5223                }
5224                ChangeEvent::EdgeAdded { .. } => {
5225                    let delta = if forward { 1 } else { -1 };
5226                    edge_delta += delta;
5227                    if forward {
5228                        added_edges = added_edges.saturating_add(1);
5229                    }
5230                }
5231                ChangeEvent::EdgeRemoved { .. } => {
5232                    let delta = if forward { -1 } else { 1 };
5233                    edge_delta += delta;
5234                    if !forward {
5235                        added_edges = added_edges.saturating_add(1);
5236                    }
5237                }
5238                ChangeEvent::SetFormula {
5239                    addr, old_formula, ..
5240                } if (forward || old_formula.is_some()) => {
5241                    formula_cells.insert((addr.sheet_id, addr.coord.row(), addr.coord.col()));
5242                }
5243                _ => {}
5244            }
5245        }
5246        let stats = self.graph.baseline_stats();
5247        let final_vertices = i128::try_from(stats.graph_vertex_count)
5248            .ok()
5249            .and_then(|count| count.checked_add(vertex_delta))
5250            .and_then(|count| usize::try_from(count).ok())
5251            .ok_or_else(|| {
5252                crate::engine::EditorError::Excel(
5253                    ExcelError::new(ExcelErrorKind::NImpl)
5254                        .with_message("replay vertex count overflow"),
5255                )
5256            })?;
5257        let final_edges = i128::try_from(stats.graph_edge_count)
5258            .ok()
5259            .and_then(|count| count.checked_add(edge_delta))
5260            .and_then(|count| usize::try_from(count).ok())
5261            .ok_or_else(|| {
5262                crate::engine::EditorError::Excel(
5263                    ExcelError::new(ExcelErrorKind::NImpl)
5264                        .with_message("replay edge count overflow"),
5265                )
5266            })?;
5267        self.preflight_graph_admission(crate::engine::resource_ledger::GraphAdmission {
5268            final_vertices,
5269            final_edges,
5270            materialization_cells: formula_cells.len() as u64,
5271            added_vertices,
5272            added_edges,
5273        })
5274        .map_err(crate::engine::EditorError::Excel)
5275    }
5276
5277    /// Undo the last group still retained by the provided audit log.
5278    ///
5279    /// Disabled, zero-cap, and evicted history is unavailable on this index-based path. Use an
5280    /// explicit `ActionJournal` with `undo_action` when undo must be independent of audit retention.
5281    pub fn undo_logged(
5282        &mut self,
5283        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5284        log: &mut crate::engine::ChangeLog,
5285    ) -> Result<(), crate::engine::EditorError> {
5286        let pending_events = log
5287            .last_group_indices()
5288            .into_iter()
5289            .map(|index| log.events()[index].clone())
5290            .collect::<Vec<_>>();
5291        self.preflight_replay_admission(&pending_events, false)?;
5292        let invalidation_baseline = self.invalidation_baseline();
5293        // UndoEngine can fail after partially applying the batch, so publish
5294        // invalidation before replay rather than only on the success path.
5295        self.invalidate_for_change_events(
5296            &pending_events,
5297            LoggedEditDirection::InverseReplay,
5298            invalidation_baseline,
5299        );
5300        let batch = undo.undo(&mut self.graph, log)?;
5301        for item in batch.iter().rev() {
5302            self.apply_inverse_row_visibility_event(&item.event);
5303            self.apply_inverse_staged_formula_event(&item.event);
5304        }
5305        if !batch.is_empty() {
5306            let events = batch
5307                .iter()
5308                .map(|item| item.event.clone())
5309                .collect::<Vec<_>>();
5310            self.clear_logged_cell_format_states(&events);
5311        }
5312        self.mirror_undo_batch_to_arrow(&batch);
5313        if !batch.is_empty() {
5314            for item in &batch {
5315                self.record_change_for_event(&item.event);
5316            }
5317        }
5318        crate::engine::trace::fz_event!(
5319            tracing::Level::INFO,
5320            "history",
5321            "history.replay",
5322            op = "undo",
5323            events_replayed = batch.len(),
5324            ownership_resyncs = batch.len()
5325        );
5326        Ok(())
5327    }
5328
5329    pub fn redo_logged(
5330        &mut self,
5331        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5332        log: &mut crate::engine::ChangeLog,
5333    ) -> Result<(), crate::engine::EditorError> {
5334        let pending_events = undo.pending_redo_events();
5335        self.preflight_replay_admission(&pending_events, true)?;
5336        let invalidation_baseline = self.invalidation_baseline();
5337        self.invalidate_for_change_events(
5338            &pending_events,
5339            LoggedEditDirection::ForwardReplay,
5340            invalidation_baseline,
5341        );
5342        let batch = undo.redo(&mut self.graph, log)?;
5343        for item in &batch {
5344            self.apply_forward_row_visibility_event(&item.event);
5345            self.apply_forward_staged_formula_event(&item.event);
5346        }
5347        if !batch.is_empty() {
5348            let events = batch
5349                .iter()
5350                .map(|item| item.event.clone())
5351                .collect::<Vec<_>>();
5352            self.clear_logged_cell_format_states(&events);
5353        }
5354        self.mirror_redo_batch_to_arrow(&batch);
5355        if !batch.is_empty() {
5356            for item in &batch {
5357                self.record_change_for_event(&item.event);
5358            }
5359        }
5360        crate::engine::trace::fz_event!(
5361            tracing::Level::INFO,
5362            "history",
5363            "history.replay",
5364            op = "redo",
5365            events_replayed = batch.len(),
5366            ownership_resyncs = batch.len()
5367        );
5368        Ok(())
5369    }
5370
5371    /// Undo the last committed atomic action using the journal stack.
5372    ///
5373    /// This path does not require a `ChangeLog`.
5374    pub fn undo_action(
5375        &mut self,
5376        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5377    ) -> Result<(), crate::engine::EditorError> {
5378        let Some(journal) = undo.pop_undo_action() else {
5379            return Ok(());
5380        };
5381        if let Err(error) = self.preflight_replay_admission(&journal.graph.events, false) {
5382            undo.push_done_action(journal);
5383            return Err(error);
5384        }
5385        let invalidation_baseline = self.invalidation_baseline();
5386
5387        self.invalidate_for_action_journal(
5388            &journal,
5389            LoggedEditDirection::InverseReplay,
5390            invalidation_baseline,
5391        );
5392        journal.graph.undo(&mut self.graph)?;
5393        self.apply_inverse_row_visibility_events(&journal.graph.events);
5394        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ true);
5395        if !journal.graph.is_empty() || !journal.arrow.is_empty() {
5396            for event in &journal.graph.events {
5397                self.record_change_for_event(event);
5398            }
5399        }
5400
5401        #[cfg(feature = "tracing")]
5402        let events_replayed = journal.graph.events.len();
5403        crate::engine::trace::fz_event!(
5404            tracing::Level::INFO,
5405            "history",
5406            "history.replay",
5407            op = "undo",
5408            events_replayed,
5409            ownership_resyncs = events_replayed
5410        );
5411        undo.push_redo_action(journal);
5412        Ok(())
5413    }
5414
5415    /// Redo the last undone atomic action using the journal stack.
5416    ///
5417    /// This path does not require a `ChangeLog`.
5418    pub fn redo_action(
5419        &mut self,
5420        undo: &mut crate::engine::graph::editor::undo_engine::UndoEngine,
5421    ) -> Result<(), crate::engine::EditorError> {
5422        let Some(journal) = undo.pop_redo_action() else {
5423            return Ok(());
5424        };
5425        if let Err(error) = self.preflight_replay_admission(&journal.graph.events, true) {
5426            undo.push_redo_action(journal);
5427            return Err(error);
5428        }
5429        let invalidation_baseline = self.invalidation_baseline();
5430        self.invalidate_for_action_journal(
5431            &journal,
5432            LoggedEditDirection::ForwardReplay,
5433            invalidation_baseline,
5434        );
5435        journal.graph.redo(&mut self.graph)?;
5436        self.apply_forward_row_visibility_events(&journal.graph.events);
5437        self.apply_arrow_undo_batch(&journal.arrow, /*undo=*/ false);
5438        if !journal.graph.is_empty() || !journal.arrow.is_empty() {
5439            for event in &journal.graph.events {
5440                self.record_change_for_event(event);
5441            }
5442        }
5443
5444        #[cfg(feature = "tracing")]
5445        let events_replayed = journal.graph.events.len();
5446        crate::engine::trace::fz_event!(
5447            tracing::Level::INFO,
5448            "history",
5449            "history.replay",
5450            op = "redo",
5451            events_replayed,
5452            ownership_resyncs = events_replayed
5453        );
5454        undo.push_done_action(journal);
5455        Ok(())
5456    }
5457
5458    fn cellref_to_sheet_row_col(&self, addr: &crate::reference::CellRef) -> (String, u32, u32) {
5459        let sheet = self.graph.sheet_name(addr.sheet_id).to_string();
5460        // Coord stores 0-based indices.
5461        let row = addr.coord.row() + 1;
5462        let col = addr.coord.col() + 1;
5463        (sheet, row, col)
5464    }
5465
5466    fn mirror_undo_batch_to_arrow(
5467        &mut self,
5468        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
5469    ) {
5470        // Undo applies inverses in reverse order.
5471        for item in batch.iter().rev() {
5472            self.mirror_inverse_change_to_arrow(&item.event);
5473        }
5474    }
5475
5476    fn mirror_redo_batch_to_arrow(
5477        &mut self,
5478        batch: &[crate::engine::graph::editor::undo_engine::UndoBatchItem],
5479    ) {
5480        // Redo applies events in forward order.
5481        for item in batch.iter() {
5482            self.mirror_forward_change_to_arrow(&item.event);
5483        }
5484    }
5485
5486    fn mirror_inverse_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
5487        use crate::engine::ChangeEvent;
5488        use formualizer_common::LiteralValue;
5489
5490        match ev {
5491            ChangeEvent::SetValue {
5492                addr,
5493                old_value,
5494                old_formula,
5495                ..
5496            } => {
5497                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5498                if old_formula.is_some() {
5499                    self.clear_delta_overlay_cell(&sheet, row, col);
5500                } else {
5501                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
5502                    self.mirror_value_to_overlay(&sheet, row, col, &v);
5503                }
5504            }
5505            ChangeEvent::SetFormula {
5506                addr,
5507                old_value,
5508                old_formula,
5509                ..
5510            } => {
5511                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5512                if old_formula.is_some() {
5513                    self.clear_delta_overlay_cell(&sheet, row, col);
5514                } else {
5515                    let v = old_value.clone().unwrap_or(LiteralValue::Empty);
5516                    self.mirror_value_to_overlay(&sheet, row, col, &v);
5517                }
5518            }
5519            ChangeEvent::SpillCommitted { old, new, .. } => {
5520                // Inverse: restore `old` (or clear if none).
5521                self.mirror_spill_snapshot(new, /*clear_only=*/ true);
5522                if let Some(snap) = old {
5523                    self.mirror_spill_snapshot(snap, /*clear_only=*/ false);
5524                }
5525            }
5526            ChangeEvent::SpillCleared { old, .. } => {
5527                // Inverse: restore prior spill.
5528                self.mirror_spill_snapshot(old, /*clear_only=*/ false);
5529            }
5530            ChangeEvent::SetRowVisibility { .. } => {
5531                // Engine-side metadata only; no Arrow overlay effect.
5532            }
5533            _ => {}
5534        }
5535    }
5536
5537    fn mirror_forward_change_to_arrow(&mut self, ev: &crate::engine::ChangeEvent) {
5538        use crate::engine::ChangeEvent;
5539
5540        match ev {
5541            ChangeEvent::SetValue { addr, new, .. } => {
5542                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5543                self.mirror_value_to_overlay(&sheet, row, col, new);
5544            }
5545            ChangeEvent::SetFormula { addr, .. } => {
5546                let (sheet, row, col) = self.cellref_to_sheet_row_col(addr);
5547                self.clear_delta_overlay_cell(&sheet, row, col);
5548                // Keep any computed overlay for this cell as-is; it will be recomputed on demand.
5549            }
5550            ChangeEvent::SpillCommitted { old, new, .. } => {
5551                if let Some(snap) = old {
5552                    self.mirror_spill_snapshot(snap, /*clear_only=*/ true);
5553                }
5554                self.mirror_spill_snapshot(new, /*clear_only=*/ false);
5555            }
5556            ChangeEvent::SpillCleared { old, .. } => {
5557                self.mirror_spill_snapshot(old, /*clear_only=*/ true);
5558            }
5559            ChangeEvent::SetRowVisibility { .. } => {
5560                // Engine-side metadata only; no Arrow overlay effect.
5561            }
5562            _ => {
5563                // Other graph structural operations do not have direct value effects in Arrow.
5564            }
5565        }
5566    }
5567
5568    fn mirror_spill_snapshot(
5569        &mut self,
5570        snap: &crate::engine::graph::editor::change_log::SpillSnapshot,
5571        clear_only: bool,
5572    ) {
5573        use formualizer_common::LiteralValue;
5574
5575        let mut i = 0usize;
5576        for row in &snap.values {
5577            for v in row {
5578                if let Some(cell) = snap.target_cells.get(i) {
5579                    let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
5580                    let out = if clear_only {
5581                        LiteralValue::Empty
5582                    } else {
5583                        v.clone()
5584                    };
5585                    self.mirror_value_to_computed_overlay(&sheet, r, c, &out);
5586                }
5587                i += 1;
5588            }
5589        }
5590        // If target_cells is longer than values (should not happen), clear remaining cells.
5591        if clear_only {
5592            for cell in snap.target_cells.iter().skip(i) {
5593                let (sheet, r, c) = self.cellref_to_sheet_row_col(cell);
5594                self.mirror_value_to_computed_overlay(&sheet, r, c, &LiteralValue::Empty);
5595            }
5596        }
5597    }
5598
5599    pub fn set_default_sheet_by_name(&mut self, name: &str) {
5600        self.graph.set_default_sheet_by_name(name);
5601    }
5602
5603    pub fn set_default_sheet_by_id(&mut self, id: SheetId) {
5604        self.graph.set_default_sheet_by_id(id);
5605    }
5606
5607    pub fn set_sheet_index_mode(&mut self, mode: crate::engine::SheetIndexMode) {
5608        self.graph.set_sheet_index_mode(mode);
5609    }
5610
5611    #[cfg(feature = "test-support")]
5612    #[doc(hidden)]
5613    pub fn mark_all_formulas_dirty_for_test(&mut self) {
5614        self.mark_all_formula_vertices_dirty();
5615    }
5616
5617    #[cfg(feature = "test-support")]
5618    #[doc(hidden)]
5619    pub fn take_criteria_mask_work_for_test() -> (usize, usize) {
5620        criteria_mask_test_hooks::take_mask_work()
5621    }
5622
5623    #[cfg(feature = "test-support")]
5624    #[doc(hidden)]
5625    pub fn lookup_index_cache_report_for_test(&self) -> LookupIndexCacheReport {
5626        self.lookup_index_cache.report()
5627    }
5628
5629    #[cfg(any(test, feature = "benchmark_internal"))]
5630    #[doc(hidden)]
5631    pub fn reset_recalc_reuse_probe(&mut self) {
5632        *self.recalc_reuse_probe.get_mut().unwrap() = RecalcReuseProbe::default();
5633    }
5634
5635    #[cfg(any(test, feature = "benchmark_internal"))]
5636    #[doc(hidden)]
5637    pub fn recalc_reuse_probe(&self) -> RecalcReuseProbe {
5638        let mut probe = self.recalc_reuse_probe.lock().unwrap().clone();
5639        if let Some(cached) = self.cached_static_schedule.as_ref() {
5640            let entry_bytes = |e: &CachedScheduleEntry| {
5641                std::mem::size_of::<CachedScheduleEntry>()
5642                    + e.candidate_vertices.heap_bytes()
5643                    + std::mem::size_of::<crate::engine::Schedule>()
5644                    + 2 * std::mem::size_of::<usize>()
5645                    + schedule_probe_retained_bytes(&e.schedule)
5646            };
5647            probe.schedule_retained_bytes = entry_bytes(cached)
5648                + self.recent_schedules.iter().map(entry_bytes).sum::<usize>()
5649                + self.base_schedule.as_ref().map_or(0, entry_bytes)
5650                + self.recent_schedules.capacity() * std::mem::size_of::<CachedScheduleEntry>();
5651        }
5652        probe
5653    }
5654
5655    #[cfg(test)]
5656    pub(crate) fn cached_static_schedule_for_test(&self) -> Option<Arc<crate::engine::Schedule>> {
5657        self.cached_static_schedule
5658            .as_ref()
5659            .map(|cached| Arc::clone(&cached.schedule))
5660    }
5661
5662    fn clear_cached_static_schedule(&mut self) {
5663        self.cached_static_schedule = None;
5664        self.recent_schedules.clear();
5665        self.base_schedule = None;
5666    }
5667
5668    /// Keep a replaced schedule among the recent ones when it is still
5669    /// current and small; drop stale ones and the oldest beyond the bounds.
5670    fn retain_recent_schedule(&mut self, entry: CachedScheduleEntry) {
5671        const RECENT_SCHEDULES: usize = 8;
5672        const RECENT_SCHEDULE_VERTICES: usize = 65_536;
5673        let revision = self.schedule_cache_authority_revision();
5674        let epoch = self.topology_epoch;
5675        self.recent_schedules
5676            .retain(|e| e.topology_epoch == epoch && e.authority_revision == revision);
5677        let current =
5678            |e: &CachedScheduleEntry| e.topology_epoch == epoch && e.authority_revision == revision;
5679        if self.base_schedule.as_ref().is_some_and(|b| !current(b)) {
5680            self.base_schedule = None;
5681        }
5682        // The largest current schedule becomes the base; a replaced base
5683        // may still join the recent ones.
5684        let mut entry = entry;
5685        if current(&entry)
5686            && entry.candidate_vertices.len() > BASE_SCHEDULE_MIN_VERTICES
5687            && self
5688                .base_schedule
5689                .as_ref()
5690                .is_none_or(|b| entry.candidate_vertices.len() > b.candidate_vertices.len())
5691        {
5692            match self.base_schedule.replace(entry) {
5693                Some(previous) => entry = previous,
5694                None => return,
5695            }
5696        }
5697        if entry.topology_epoch != epoch
5698            || entry.authority_revision != revision
5699            || entry.candidate_vertices.len() > RECENT_SCHEDULE_VERTICES / 4
5700        {
5701            return;
5702        }
5703        self.recent_schedules.push(entry);
5704        let mut total: usize = self
5705            .recent_schedules
5706            .iter()
5707            .map(|e| e.candidate_vertices.len())
5708            .sum();
5709        while self.recent_schedules.len() > RECENT_SCHEDULES || total > RECENT_SCHEDULE_VERTICES {
5710            let oldest = self.recent_schedules.remove(0);
5711            total -= oldest.candidate_vertices.len();
5712        }
5713    }
5714
5715    fn invalidation_baseline(&self) -> InvalidationBaseline {
5716        InvalidationBaseline {
5717            snapshot_id: self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed),
5718            topology_epoch: self.topology_epoch,
5719        }
5720    }
5721
5722    fn classify_change_events(
5723        events: &[crate::engine::ChangeEvent],
5724        direction: LoggedEditDirection,
5725    ) -> LoggedEditImpact {
5726        use crate::engine::ChangeEvent;
5727
5728        events
5729            .iter()
5730            .map(|event| match event {
5731                ChangeEvent::CompoundStart { .. } | ChangeEvent::CompoundEnd { .. } => {
5732                    LoggedEditImpact::NoOp
5733                }
5734                ChangeEvent::SetRowVisibility { .. }
5735                | ChangeEvent::SetValue {
5736                    old_formula: None,
5737                    old_value: Some(_),
5738                    ..
5739                } => LoggedEditImpact::DataOnly,
5740                // Original canonical writes can lack an Arrow old value while
5741                // updating an existing placeholder. Inverse replay actually
5742                // removes that vertex; a later redo recreates it.
5743                ChangeEvent::SetValue {
5744                    old_formula: None,
5745                    old_value: None,
5746                    ..
5747                } if direction == LoggedEditDirection::Original => LoggedEditImpact::DataOnly,
5748                ChangeEvent::SetValue { .. }
5749                | ChangeEvent::SetFormula { .. }
5750                | ChangeEvent::AddVertex { .. }
5751                | ChangeEvent::RemoveVertex { .. }
5752                | ChangeEvent::VertexMoved { .. }
5753                | ChangeEvent::FormulaAdjusted { .. }
5754                | ChangeEvent::NamedRangeAdjusted { .. }
5755                | ChangeEvent::EdgeAdded { .. }
5756                | ChangeEvent::EdgeRemoved { .. }
5757                | ChangeEvent::DefineName { .. }
5758                | ChangeEvent::UpdateName { .. }
5759                | ChangeEvent::DeleteName { .. }
5760                | ChangeEvent::SpillCommitted { .. }
5761                | ChangeEvent::SpillCleared { .. }
5762                | ChangeEvent::StagedFormulaCellChanged { .. } => LoggedEditImpact::Topology,
5763            })
5764            .max()
5765            .unwrap_or(LoggedEditImpact::NoOp)
5766    }
5767
5768    fn classify_arrow_undo(arrow: &crate::engine::ArrowUndoBatch) -> LoggedEditImpact {
5769        use crate::engine::ArrowOp;
5770
5771        arrow
5772            .ops
5773            .iter()
5774            .map(|op| match op {
5775                ArrowOp::SetDeltaCell { .. } | ArrowOp::SetComputedCell { .. } => {
5776                    LoggedEditImpact::DataOnly
5777                }
5778                ArrowOp::RestoreComputedRect { .. }
5779                | ArrowOp::InsertRows { .. }
5780                | ArrowOp::InsertCols { .. } => LoggedEditImpact::Topology,
5781            })
5782            .max()
5783            .unwrap_or(LoggedEditImpact::NoOp)
5784    }
5785
5786    fn apply_logged_edit_impact(
5787        &mut self,
5788        impact: LoggedEditImpact,
5789        baseline: InvalidationBaseline,
5790    ) {
5791        match impact {
5792            LoggedEditImpact::NoOp => {}
5793            LoggedEditImpact::DataOnly => {
5794                if self.topology_epoch == baseline.topology_epoch
5795                    && self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
5796                        == baseline.snapshot_id
5797                {
5798                    self.mark_data_edited();
5799                }
5800            }
5801            LoggedEditImpact::Topology => {
5802                // Some structural entry points already publish topology
5803                // invalidation. Do not bump the same batch twice.
5804                if self.topology_epoch == baseline.topology_epoch {
5805                    self.mark_topology_edited();
5806                }
5807            }
5808        }
5809    }
5810
5811    fn invalidate_for_change_events(
5812        &mut self,
5813        events: &[crate::engine::ChangeEvent],
5814        direction: LoggedEditDirection,
5815        baseline: InvalidationBaseline,
5816    ) {
5817        self.apply_logged_edit_impact(Self::classify_change_events(events, direction), baseline);
5818    }
5819
5820    fn invalidate_for_action_journal(
5821        &mut self,
5822        journal: &crate::engine::ActionJournal,
5823        direction: LoggedEditDirection,
5824        baseline: InvalidationBaseline,
5825    ) {
5826        let impact = Self::classify_change_events(&journal.graph.events, direction)
5827            .max(Self::classify_arrow_undo(&journal.arrow));
5828        self.apply_logged_edit_impact(impact, baseline);
5829    }
5830
5831    /// Mark data edited: bump snapshot and set edited flag.
5832    /// Value-only edits keep the stable-topology schedule cache alive.
5833    pub fn mark_data_edited(&mut self) {
5834        self.lookup_index_cache.clear();
5835        self.snapshot_id
5836            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5837        self.has_edited = true;
5838    }
5839
5840    /// Mark a topology-changing edit: bump snapshot + topology epoch and invalidate cached schedules.
5841    pub fn mark_topology_edited(&mut self) {
5842        self.lookup_index_cache.clear();
5843        self.snapshot_id
5844            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
5845        self.topology_epoch = self.topology_epoch.wrapping_add(1);
5846        self.graph.bump_topology_revision();
5847        self.clear_cached_static_schedule();
5848        if let Some(ledger) = self.active_resource_ledger.as_mut() {
5849            let released = ledger.account_mixed_cache(0);
5850            debug_assert!(released.is_ok());
5851        }
5852        self.has_edited = true;
5853        // Eager sync at a topology edit, so read-only (`&self`) plans and
5854        // inspection see a current authority (not during a load or an open
5855        // structural capture).
5856        self.graph.authority_sync_eager();
5857    }
5858
5859    fn mark_all_formula_vertices_dirty(&mut self) {
5860        let vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
5861        for vertex in vertices {
5862            self.graph.mark_vertex_dirty(vertex);
5863        }
5864    }
5865
5866    fn mark_moved_formula_vertices_dirty(
5867        &mut self,
5868        summary: &crate::engine::graph::editor::vertex_editor::ShiftSummary,
5869    ) {
5870        for vertex in &summary.vertices_moved {
5871            if self.graph.has_formula(*vertex) {
5872                self.graph.mark_vertex_dirty(*vertex);
5873            }
5874        }
5875    }
5876
5877    /// Access Arrow sheet store (read-only)
5878    pub fn sheet_store(&self) -> &SheetStore {
5879        &self.arrow_sheets
5880    }
5881
5882    /// True when any sheet carries manual/filter row-visibility state.
5883    /// Used by load-time freshness checks (see `Engine::adopt_file_sheets`).
5884    pub(crate) fn has_row_visibility_state(&self) -> bool {
5885        !self.row_visibility.is_empty()
5886    }
5887
5888    /// Access Arrow sheet store (mutable)
5889    pub fn sheet_store_mut(&mut self) -> &mut SheetStore {
5890        &mut self.arrow_sheets
5891    }
5892
5893    pub fn has_staged_formulas(&self) -> bool {
5894        !self.staged_formulas.is_empty()
5895    }
5896
5897    pub fn staged_formula_count(&self) -> usize {
5898        self.staged_formulas.values().map(StagedSheet::len).sum()
5899    }
5900
5901    /// Stage a formula text instead of inserting into the graph (used when deferring is enabled).
5902    pub fn stage_formula_text(&mut self, sheet: &str, row: u32, col: u32, text: String) {
5903        self.staged_formulas
5904            .entry(sheet.to_string())
5905            .or_default()
5906            .stage(row, col, text);
5907        self.staged_formula_index.stage(sheet, row, col);
5908        if let Some(sheet) = self.graph.sheet_id(sheet) {
5909            self.invalidate_pending_spills(StructuralScope::Cell {
5910                sheet,
5911                row: row.saturating_sub(1),
5912                col: col.saturating_sub(1),
5913            });
5914        }
5915    }
5916
5917    fn index_removed_staged_sheet(&mut self, sheet: &str, staged: &StagedSheet) {
5918        for (row, col, _) in &staged.entries {
5919            self.staged_formula_index.remove(sheet, *row, *col);
5920        }
5921        if staged.deferred_package.is_some() {
5922            self.staged_formula_index.set_package(sheet, None);
5923        }
5924    }
5925
5926    fn restore_staged_sheet(&mut self, sheet: String, staged: StagedSheet) {
5927        self.staged_formulas.insert(sheet, staged);
5928    }
5929
5930    #[doc(hidden)]
5931    pub fn source_formula_ingress(&mut self) -> SourceFormulaIngress<'_, R> {
5932        SourceFormulaIngress { engine: self }
5933    }
5934
5935    #[doc(hidden)]
5936    /// Test-only fault-injection seam. Not part of the supported API; it exists so
5937    /// integration tests in sibling crates can fail a commit at an exact point.
5938    #[doc(hidden)]
5939    #[cfg(any(test, feature = "test-support"))]
5940    pub fn set_before_prepared_span_commit_hook(
5941        &mut self,
5942        hook: impl FnOnce() + Send + Sync + 'static,
5943    ) {
5944        self.before_prepared_span_commit_hook = Some(Box::new(hook));
5945    }
5946
5947    #[doc(hidden)]
5948    /// Test-only fault-injection seam, matching `set_after_eager_proposal_commit_hook`.
5949    #[cfg(test)]
5950    pub(crate) fn set_before_target_preparation_commit_hook(
5951        &mut self,
5952        hook: impl FnOnce() + Send + Sync + 'static,
5953    ) {
5954        self.before_target_preparation_commit_hook = Some(Box::new(hook));
5955    }
5956
5957    fn stage_deferred_formula_package(&mut self, package: crate::engine::DeferredFormulaPackage) {
5958        if let Ok(replay) = package.replay.lock()
5959            && let Some(footprint) = replay.selection_cache_footprint()
5960            && !self
5961                .source_cache_footprints
5962                .iter()
5963                .any(|known| known.ptr_eq(&footprint))
5964        {
5965            self.source_cache_footprints.push(footprint);
5966        }
5967        let sheet = package.sheet_name.clone();
5968        let staged = self.staged_formulas.entry(sheet.clone()).or_default();
5969        debug_assert!(staged.deferred_package.is_none());
5970        staged.deferred_package = Some(package);
5971        staged.reconcile_attached_deferred_package();
5972        self.staged_formula_index
5973            .set_package(&sheet, staged.deferred_package.as_ref());
5974        if let Some(sheet_id) = self.graph.sheet_id(&sheet) {
5975            let package = self
5976                .staged_formulas
5977                .get(&sheet)
5978                .and_then(|staged| staged.deferred_package.as_ref());
5979            for &(vertex, anchor, region) in &self.blocked_pending_spills {
5980                if anchor.sheet_id == sheet_id
5981                    && self.graph.vertex_exists(vertex)
5982                    && self.graph.get_cell_ref(vertex) == Some(anchor)
5983                    && self.staged_formula_index.package_occupies_spill(
5984                        &sheet,
5985                        (anchor.coord.row() + 1, anchor.coord.col() + 1),
5986                        (
5987                            region.rows.query_bounds().1 + 1,
5988                            region.cols.query_bounds().1 + 1,
5989                        ),
5990                        |point| package.is_some_and(|package| package.suppressed.contains(&point)),
5991                    )
5992                {
5993                    self.graph.mark_vertex_dirty(vertex);
5994                }
5995            }
5996        }
5997    }
5998
5999    pub fn clear_staged_formula_text(&mut self, sheet: &str, row: u32, col: u32) -> Option<String> {
6000        let mut removed = None;
6001        let mut remove_sheet = false;
6002        let mut had_package = false;
6003        if let Some(entries) = self.staged_formulas.get_mut(sheet) {
6004            had_package = entries.deferred_package.is_some();
6005            removed = entries.remove(row, col);
6006            remove_sheet = entries.is_empty();
6007        }
6008        let ordinary_removed = self.staged_formula_index.remove(sheet, row, col);
6009        if !ordinary_removed && had_package {
6010            self.staged_formula_index.touch_package(sheet);
6011        }
6012        if remove_sheet {
6013            self.staged_formulas.remove(sheet);
6014            self.staged_formula_index.set_package(sheet, None);
6015        }
6016        if (ordinary_removed || had_package)
6017            && let Some(sheet) = self.graph.sheet_id(sheet)
6018        {
6019            self.invalidate_pending_spills(StructuralScope::Cell {
6020                sheet,
6021                row: row.saturating_sub(1),
6022                col: col.saturating_sub(1),
6023            });
6024        }
6025        removed
6026    }
6027
6028    pub fn clear_staged_formulas_for_sheet(&mut self, sheet: &str) {
6029        if self.staged_formulas.remove(sheet).is_some() {
6030            self.staged_formula_index.clear_sheet(sheet);
6031            if let Some(sheet_id) = self.graph.sheet_id(sheet) {
6032                self.invalidate_pending_spills(StructuralScope::Sheet(sheet_id));
6033            }
6034        }
6035    }
6036
6037    pub fn rename_staged_formula_sheet(&mut self, old: &str, new: &str) {
6038        let Some(entries) = self.staged_formulas.remove(old) else {
6039            return;
6040        };
6041        self.staged_formula_index.clear_sheet(old);
6042        let (formulas, mut package) = entries.into_parts();
6043        for (row, col, text) in formulas {
6044            self.stage_formula_text(new, row, col, text);
6045        }
6046        if let Some(package) = package.as_mut() {
6047            package.sheet_name = new.to_string();
6048        }
6049        if let Some(package) = package {
6050            self.stage_deferred_formula_package(package);
6051        }
6052    }
6053
6054    /// Get a staged formula text for a given cell if present (cloned).
6055    pub fn get_staged_formula_text(&self, sheet: &str, row: u32, col: u32) -> Option<String> {
6056        self.staged_formulas
6057            .get(sheet)
6058            .and_then(|v| v.get(row, col))
6059    }
6060
6061    pub fn formula_parse_diagnostics(&self) -> &[FormulaParseDiagnostic] {
6062        &self.formula_parse_diagnostics
6063    }
6064
6065    pub fn take_formula_parse_diagnostics(&mut self) -> Vec<FormulaParseDiagnostic> {
6066        std::mem::take(&mut self.formula_parse_diagnostics)
6067    }
6068
6069    pub fn clear_formula_parse_diagnostics(&mut self) {
6070        self.formula_parse_diagnostics.clear();
6071    }
6072
6073    pub fn last_formula_ingest_report(&self) -> Option<&FormulaIngestReport> {
6074        self.last_formula_ingest_report.as_ref()
6075    }
6076
6077    pub fn formula_ingest_report_total(&self) -> &FormulaIngestReport {
6078        &self.formula_ingest_report_total
6079    }
6080
6081    #[cfg(test)]
6082    pub(crate) fn set_before_target_planning_snapshot_hook_for_test(
6083        &mut self,
6084        hook: impl FnOnce() + Send + Sync + 'static,
6085    ) {
6086        self.before_target_planning_snapshot_hook = Some(Box::new(hook));
6087    }
6088
6089    #[cfg(test)]
6090    pub(crate) fn inject_target_semantic_stale_once_for_test(&mut self) {
6091        self.inject_target_semantic_stale_once_for_test = true;
6092    }
6093
6094    #[cfg(test)]
6095    pub(crate) fn force_virtual_dep_changes_for_test(&mut self, rounds: usize) {
6096        self.force_virtual_dep_changes_remaining_for_test = rounds;
6097    }
6098
6099    #[cfg(test)]
6100    pub(crate) fn fail_evaluation_commit_preflight_once_for_test(&mut self) {
6101        self.fail_evaluation_commit_preflight_once_for_test = true;
6102    }
6103
6104    #[cfg(test)]
6105    pub(crate) fn set_target_preparation_fault_for_test(
6106        &mut self,
6107        fault: crate::engine::target_preparation::TargetPreparationFault,
6108    ) {
6109        self.target_preparation_fault_for_test = Some(fault);
6110    }
6111
6112    #[cfg(test)]
6113    pub(crate) fn staged_formula_index_revision_for_test(&self) -> u64 {
6114        self.staged_formula_index.revision()
6115    }
6116
6117    #[cfg(test)]
6118    pub(crate) fn deferred_package_for_test(
6119        &self,
6120        sheet: &str,
6121    ) -> &crate::engine::DeferredFormulaPackage {
6122        self.staged_formulas
6123            .get(sheet)
6124            .unwrap()
6125            .deferred_package
6126            .as_ref()
6127            .unwrap()
6128    }
6129
6130    #[cfg(test)]
6131    pub(crate) fn staged_formula_index_is_consistent_for_test(&self) -> bool {
6132        let ordinary_storage = self
6133            .staged_formulas
6134            .values()
6135            .map(|sheet| sheet.entries.len())
6136            .sum::<usize>();
6137        let package_storage = self
6138            .staged_formulas
6139            .values()
6140            .filter(|sheet| sheet.deferred_package.is_some())
6141            .count();
6142        ordinary_storage == self.staged_formula_index.ordinary_count()
6143            && package_storage == self.staged_formula_index.package_count()
6144            && self.staged_formulas.iter().all(|(name, sheet)| {
6145                sheet.entries.iter().all(|(row, col, _)| {
6146                    let leases = self
6147                        .staged_formula_index
6148                        .leases_in_region(name, *row, *col, *row, *col);
6149                    leases.len() == 1 && leases[0].row == *row && leases[0].col == *col
6150                })
6151            })
6152    }
6153
6154    #[cfg(test)]
6155    pub(crate) fn evaluation_request_begin_count_for_test(&self) -> u64 {
6156        self.evaluation_request_begin_count_for_test
6157    }
6158
6159    #[cfg(test)]
6160    pub(crate) fn set_before_legacy_fallback_final_provider_sample_hook(
6161        &mut self,
6162        hook: impl FnOnce() + Send + Sync + 'static,
6163    ) {
6164        self.before_legacy_fallback_final_provider_sample_hook = Some(Box::new(hook));
6165    }
6166
6167    #[cfg(test)]
6168    pub(crate) fn set_after_eager_proposal_commit_hook(
6169        &mut self,
6170        hook: impl FnOnce() + Send + Sync + 'static,
6171    ) {
6172        self.after_eager_proposal_commit_hook = Some(Box::new(hook));
6173    }
6174
6175    #[cfg(test)]
6176    pub(crate) fn topology_epoch_for_test(&self) -> u64 {
6177        self.topology_epoch
6178    }
6179
6180    #[cfg(test)]
6181    pub(crate) fn graph_topology_revision_for_test(&self) -> u64 {
6182        self.graph.topology_revision()
6183    }
6184
6185    fn record_formula_ingest_report(&mut self, report: FormulaIngestReport) {
6186        self.formula_ingest_report_total.mode = report.mode;
6187        self.formula_ingest_report_total.accumulate(&report);
6188        self.last_formula_ingest_report = Some(report);
6189    }
6190
6191    fn collect_planning_function_requests(
6192        ast: &ASTNode,
6193        requests: &mut Vec<(String, String, usize)>,
6194    ) {
6195        match &ast.node_type {
6196            ASTNodeType::Function { name, args } => {
6197                requests.push((String::new(), name.clone(), args.len()));
6198                for arg in args {
6199                    Self::collect_planning_function_requests(arg, requests);
6200                }
6201            }
6202            ASTNodeType::BinaryOp { left, right, .. } => {
6203                Self::collect_planning_function_requests(left, requests);
6204                Self::collect_planning_function_requests(right, requests);
6205            }
6206            ASTNodeType::UnaryOp { expr, .. } => {
6207                Self::collect_planning_function_requests(expr, requests);
6208            }
6209            ASTNodeType::Call { callee, args } => {
6210                Self::collect_planning_function_requests(callee, requests);
6211                for arg in args {
6212                    Self::collect_planning_function_requests(arg, requests);
6213                }
6214            }
6215            ASTNodeType::Array(rows) => {
6216                for cell in rows.iter().flatten() {
6217                    Self::collect_planning_function_requests(cell, requests);
6218                }
6219            }
6220            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => {}
6221        }
6222    }
6223
6224    fn prepared_function_semantics_changed(
6225        &self,
6226        preparation: &crate::engine::FormulaCompressedPreparation,
6227        guard: &crate::function_registry::SemanticEpochReadGuard,
6228    ) -> bool {
6229        if preparation.function_semantic_epoch == guard.epoch() {
6230            return false;
6231        }
6232
6233        guard
6234            .semantic_changes_affect_requests_since(preparation.function_semantic_epoch, Vec::new())
6235    }
6236
6237    pub(crate) fn prepare_source_formula_families(
6238        &mut self,
6239        sheet_name: &str,
6240        families: &[crate::engine::SourceFormulaFamily],
6241    ) -> crate::engine::FormulaCompressedPreparation {
6242        crate::engine::FormulaCompressedPreparation {
6243            engine_token: Arc::clone(&self.source_formula_token),
6244            function_semantic_epoch: crate::function_registry::semantic_epoch(),
6245            function_provider_revision: None,
6246            function_semantics_used: false,
6247            sheet_name: Arc::from(sheet_name),
6248            rejected: BTreeMap::new(),
6249            eager_replay: Vec::new(),
6250            preparation_spool_replays: 0,
6251            clean_rejected_anchor_counts: [0; 3],
6252            fragmented_rejected_anchor_counts: [0; 3],
6253            exact_replay: None,
6254            replay_disposition: crate::engine::FormulaReplayDisposition::default(),
6255        }
6256    }
6257
6258    fn prepare_source_formula_proposals(
6259        &mut self,
6260        sheet_name: &str,
6261        families: &[crate::engine::SourceFormulaFamily],
6262        authority_partitions: &[crate::engine::PartitionedSourceFormulaFamily],
6263        replay_partitions: &[crate::engine::PartitionedSourceFormulaFamily],
6264        formula_record_count: u64,
6265        replay: Arc<std::sync::Mutex<Box<dyn crate::engine::DeferredFormulaReplay>>>,
6266        suppressed: &BTreeSet<(u32, u32)>,
6267        consumed: &FxHashSet<(crate::engine::SourceFamilyId, crate::engine::SourceCoord)>,
6268        consumed_engine: Option<&Arc<()>>,
6269    ) -> Result<crate::engine::FormulaCompressedPreparation, ExcelError> {
6270        let mut preparation = self.prepare_source_formula_families(sheet_name, families);
6271        preparation.exact_replay = Some(Arc::clone(&replay));
6272        preparation
6273            .replay_disposition
6274            .register_consumed_members(consumed_engine, consumed.iter().copied());
6275        if !preparation
6276            .replay_disposition
6277            .consumed_engine_matches(&self.source_formula_token)
6278        {
6279            return Err(ExcelError::new(ExcelErrorKind::Value)
6280                .with_message("ResidualConsumedEngineMismatch"));
6281        }
6282        preparation
6283            .replay_disposition
6284            .extend_suppressed_excel_coords(suppressed.iter().copied());
6285        Ok(preparation)
6286    }
6287
6288    fn formula_batch_from_exact_replay(
6289        &mut self,
6290        sheet_name: &str,
6291        replayed: impl IntoIterator<Item = crate::engine::DeferredReplayFormula>,
6292    ) -> Result<FormulaIngestBatch, ExcelError> {
6293        let mut cache = rustc_hash::FxHashMap::default();
6294        let mut formulas = Vec::new();
6295        for record in replayed {
6296            let key = if record.text.starts_with('=') {
6297                record.text
6298            } else {
6299                format!("={}", record.text)
6300            };
6301            let ast_id = if let Some(cached) = cache.get(&key) {
6302                *cached
6303            } else {
6304                let parsed = match formualizer_parse::parser::parse(&key) {
6305                    Ok(parsed) => parsed,
6306                    Err(error) => {
6307                        let Some(parsed) = self.handle_formula_parse_error(
6308                            sheet_name,
6309                            record.row,
6310                            record.col,
6311                            &key,
6312                            error.to_string(),
6313                        )?
6314                        else {
6315                            continue;
6316                        };
6317                        parsed
6318                    }
6319                };
6320                let ast_id = self.intern_formula_ast(&parsed);
6321                cache.insert(key.clone(), ast_id);
6322                ast_id
6323            };
6324            formulas.push(
6325                FormulaIngestRecord::new(record.row, record.col, ast_id, Some(Arc::from(key)))
6326                    .with_source_proof(record.source_order, record.family, record.partition_owner),
6327            );
6328        }
6329        Ok(FormulaIngestBatch::new(sheet_name.to_string(), formulas))
6330    }
6331
6332    fn prepare_target_combined_legacy_graph(
6333        &self,
6334        packages: &[PreparedTargetSourcePackage],
6335        ordinary: &[PreparedOrdinaryStagedFormula],
6336    ) -> Result<(PreparedLegacyGraphPlan, usize), ExcelError> {
6337        let mut planned_by_coord = BTreeMap::new();
6338        for package in packages {
6339            for (row, col, ast_id, plan) in &package.legacy {
6340                planned_by_coord.insert((package.sheet_id, *row, *col), (*ast_id, plan.clone()));
6341            }
6342        }
6343        for formula in ordinary {
6344            if let Some((ast_id, plan)) = formula.ast_id.zip(formula.plan.clone()) {
6345                planned_by_coord.insert(
6346                    (formula.sheet_id, formula.lease.row, formula.lease.col),
6347                    (ast_id, plan),
6348                );
6349            }
6350        }
6351        let planned = planned_by_coord
6352            .into_iter()
6353            .map(|((sheet_id, row, col), (ast_id, plan))| (sheet_id, row, col, ast_id, plan))
6354            .collect::<Vec<_>>();
6355        let formula_count = planned.len();
6356        let graph = self
6357            .graph
6358            .prepare_legacy_graph_plan_multi_sheet(planned)
6359            .map_err(|error| {
6360                ExcelError::new(ExcelErrorKind::Value)
6361                    .with_message(format!("target graph preparation failed: {error}"))
6362            })?;
6363        Ok((graph, formula_count))
6364    }
6365
6366    fn replay_target_coordinates(
6367        &mut self,
6368        replay: &Arc<std::sync::Mutex<Box<dyn crate::engine::DeferredFormulaReplay>>>,
6369        coordinates: &[(u32, u32)],
6370        deadline: Option<std::time::Instant>,
6371        scratch: &mut u64,
6372    ) -> Result<Option<Vec<crate::engine::DeferredReplayFormula>>, ExcelError> {
6373        if coordinates.is_empty() {
6374            return Ok(Some(Vec::new()));
6375        }
6376        let mut guard = replay.lock().map_err(|_| {
6377            ExcelError::new(ExcelErrorKind::Value)
6378                .with_message("deferred formula spool lock poisoned")
6379        })?;
6380        let retained = guard.selection_cache_footprint().is_some();
6381        let records = guard.replay_selected_exact(coordinates, &mut |work, bytes| {
6382            self.target_preparation_checkpoint(deadline, work)?;
6383            if retained && let Some(ledger) = self.active_resource_ledger.as_mut() {
6384                ledger
6385                    .reserve_retained(bytes)
6386                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
6387                self.source_cache_accounted = self.source_cache_accounted.saturating_add(bytes);
6388            }
6389            self.reserve_graph_source_scratch(bytes)?;
6390            *scratch = scratch.saturating_add(bytes);
6391            Ok(())
6392        });
6393        drop(guard);
6394        let reconciled = self.reconcile_source_cache_footprints();
6395        let records = records?;
6396        reconciled?;
6397        Ok(records)
6398    }
6399
6400    fn prepare_target_exact_source_selection(
6401        &mut self,
6402        sheet: &str,
6403        lease: StagedPackageLease,
6404        coordinates: Vec<(u32, u32)>,
6405        previous: &BTreeSet<(u32, u32)>,
6406        allow_partial_shared: bool,
6407        deadline: Option<std::time::Instant>,
6408        scratch: &mut u64,
6409    ) -> Result<Option<PreparedTargetSourcePackage>, ExcelError> {
6410        let package = self
6411            .staged_formulas
6412            .get(sheet)
6413            .and_then(|s| s.deferred_package.as_ref())
6414            .unwrap();
6415        if !package.source_geometry_complete
6416            || ((!package.families.is_empty() || !package.partitioned_families.is_empty())
6417                && !package.coordinates_cover_families)
6418            || package.reconciliation_replay.is_some()
6419        {
6420            return Ok(None);
6421        }
6422        let selected_points: BTreeSet<_> = coordinates
6423            .into_iter()
6424            .filter(|point| !package.suppressed.contains(point))
6425            .collect();
6426        let mut prepared = PreparedTargetSourcePackage::empty_selection(
6427            sheet,
6428            self.graph.sheet_id(sheet).unwrap(),
6429            lease,
6430        );
6431        if selected_points.is_empty() {
6432            return Ok(Some(prepared));
6433        }
6434        let mut routing = crate::engine::FormulaReplayDisposition::default();
6435        for partition in &package.partitioned_families {
6436            routing
6437                .register_partition(partition, false)
6438                .map_err(|message| ExcelError::new(ExcelErrorKind::Value).with_message(message))?;
6439        }
6440        routing.extend_suppressed_excel_coords(package.suppressed.iter().copied());
6441        routing.register_consumed_members(
6442            package.consumed_engine.as_ref(),
6443            package.consumed_members.iter().copied(),
6444        );
6445        let replay = Arc::clone(&package.replay);
6446        let source_report = package.accounting_report();
6447        prepared.source_report = source_report;
6448        prepared.disposition = routing.clone();
6449
6450        let replay_points: BTreeSet<_> = selected_points
6451            .iter()
6452            .copied()
6453            .filter(|&(row, col)| !prepared.direct_contains(row, col))
6454            .collect();
6455        let coordinates: Vec<_> = replay_points.iter().copied().collect();
6456        let records = self.replay_target_coordinates(&replay, &coordinates, deadline, scratch)?;
6457        prepared.spool_replays = u64::from(!coordinates.is_empty());
6458        let Some(mut replay_records) = records else {
6459            return Ok(None);
6460        };
6461        for record in &mut replay_records {
6462            if record.family.is_none() {
6463                record.partition_owner = routing
6464                    .ordinary_disposition(crate::engine::SourceCoord {
6465                        row: record.row.saturating_sub(1),
6466                        col: record.col.saturating_sub(1),
6467                    })
6468                    .1;
6469            }
6470        }
6471        replay_records.sort_by_key(|record| record.source_order);
6472        if replay_records
6473            .iter()
6474            .any(|record| !selected_points.contains(&(record.row, record.col)))
6475            || replay_records
6476                .windows(2)
6477                .any(|records| records[0].source_order == records[1].source_order)
6478        {
6479            return Err(ExcelError::new(ExcelErrorKind::Value)
6480                .with_message("invalid indexed exact source selection"));
6481        }
6482        let represented: BTreeSet<_> = replay_records.iter().map(|r| (r.row, r.col)).collect();
6483        if represented != replay_points {
6484            return Err(ExcelError::new(ExcelErrorKind::Value)
6485                .with_message("incomplete indexed exact source selection"));
6486        }
6487        // The last source record must agree with compressed ownership evidence.
6488        // Earlier overridden records retain ordering but demand no dependencies.
6489        let source = self
6490            .staged_formulas
6491            .get(sheet)
6492            .unwrap()
6493            .deferred_package
6494            .as_ref()
6495            .unwrap();
6496        let contains = |rect: crate::engine::SourceRect, coord: crate::engine::SourceCoord| {
6497            coord.row >= rect.start.row
6498                && coord.row <= rect.end.row
6499                && coord.col >= rect.start.col
6500                && coord.col <= rect.end.col
6501        };
6502        let mut checked = BTreeSet::new();
6503        for record in replay_records.iter().rev() {
6504            if !checked.insert((record.row, record.col)) {
6505                continue;
6506            }
6507            let coord = crate::engine::SourceCoord {
6508                row: record.row - 1,
6509                col: record.col - 1,
6510            };
6511            let agrees = |owner, shared: bool| {
6512                record.family == shared.then_some(owner)
6513                    && record.partition_owner.or(record.family) == Some(owner)
6514            };
6515            let mut valid = true;
6516            for family in &source.families {
6517                let owns = match &family.members {
6518                    crate::engine::SourceFamilyMembers::CompleteDomain(domain) => {
6519                        contains(domain.rect(), coord)
6520                    }
6521                    crate::engine::SourceFamilyMembers::ExplicitMembers(members) => {
6522                        members.as_slice().binary_search(&coord).is_ok()
6523                    }
6524                };
6525                if owns && !agrees(family.source_id, true) {
6526                    valid = false;
6527                }
6528            }
6529            for family in &source.partitioned_families {
6530                if family
6531                    .fragments
6532                    .iter()
6533                    .any(|fragment| contains(fragment.rect(), coord))
6534                    && !agrees(family.source_id, true)
6535                {
6536                    valid = false;
6537                }
6538                if let Some(member) = family
6539                    .legacy_members
6540                    .as_slice()
6541                    .iter()
6542                    .find(|member| member.coord == coord)
6543                    && !agrees(
6544                        family.source_id,
6545                        member.kind == crate::engine::PartitionLegacyMemberKind::SharedFamilyMember,
6546                    )
6547                {
6548                    valid = false;
6549                }
6550            }
6551            if !valid {
6552                return Err(ExcelError::new(ExcelErrorKind::Value)
6553                    .with_message("indexed source ownership mismatch"));
6554            }
6555        }
6556        prepared.selected_points = Some(selected_points);
6557        prepared.replay_records = replay_records;
6558        Ok(Some(prepared))
6559    }
6560
6561    fn prepare_target_source_package(
6562        &mut self,
6563        sheet: &str,
6564        lease: StagedPackageLease,
6565        deadline: Option<std::time::Instant>,
6566    ) -> Result<PreparedTargetSourcePackage, ExcelError> {
6567        let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
6568            ExcelError::new(ExcelErrorKind::Ref)
6569                .with_message(format!("deferred source sheet not found: {sheet}"))
6570        })?;
6571        let (
6572            source_report,
6573            families,
6574            partitions,
6575            replay,
6576            invalidated,
6577            suppressed,
6578            consumed_members,
6579            consumed_engine,
6580            reconciliation_replay,
6581        ) = {
6582            let package = self
6583                .staged_formulas
6584                .get(sheet)
6585                .and_then(|staged| staged.deferred_package.as_ref())
6586                .ok_or_else(|| {
6587                    ExcelError::new(ExcelErrorKind::Value)
6588                        .with_message("staged deferred source package is unavailable")
6589                })?;
6590            if package.sheet_name != sheet {
6591                return Err(ExcelError::new(ExcelErrorKind::Value)
6592                    .with_message("deferred formula package sheet mismatch"));
6593            }
6594            (
6595                package.accounting_report(),
6596                package.families.clone(),
6597                package.partitioned_families.clone(),
6598                Arc::clone(&package.replay),
6599                package.invalidated.clone(),
6600                package.suppressed.clone(),
6601                package.consumed_members.clone(),
6602                package.consumed_engine.clone(),
6603                package.reconciliation_replay.clone(),
6604            )
6605        };
6606
6607        let mut replay_disposition = crate::engine::FormulaReplayDisposition::default();
6608        for partition in &partitions {
6609            replay_disposition
6610                .register_partition(partition, false)
6611                .map_err(|reason| ExcelError::new(ExcelErrorKind::Value).with_message(reason))?;
6612        }
6613        replay_disposition.extend_suppressed_excel_coords(suppressed.iter().copied());
6614        replay_disposition
6615            .register_consumed_members(consumed_engine.as_ref(), consumed_members.iter().copied());
6616        if !replay_disposition.consumed_engine_matches(&self.source_formula_token) {
6617            return Err(ExcelError::new(ExcelErrorKind::Value)
6618                .with_message("ResidualConsumedEngineMismatch"));
6619        }
6620        self.target_preparation_checkpoint(deadline, 1)?;
6621        let mut replay_records = if let Some(mut records) = reconciliation_replay {
6622            records.retain(|record| {
6623                let Some((row, col)) = record.row.checked_sub(1).zip(record.col.checked_sub(1))
6624                else {
6625                    return true;
6626                };
6627                let coord = crate::engine::SourceCoord { row, col };
6628                let disposition = record.family.map_or_else(
6629                    || replay_disposition.ordinary_disposition(coord).0,
6630                    |family| replay_disposition.shared_disposition(family, coord),
6631                );
6632                !matches!(
6633                    disposition,
6634                    crate::engine::FormulaReplayCoordinateDisposition::Direct
6635                        | crate::engine::FormulaReplayCoordinateDisposition::Suppressed
6636                )
6637            });
6638            records
6639        } else {
6640            replay
6641                .lock()
6642                .map_err(|_| {
6643                    ExcelError::new(ExcelErrorKind::Value)
6644                        .with_message("deferred formula spool lock poisoned")
6645                })?
6646                .replay_partitioned(&replay_disposition, &partitions)
6647                .map_err(|message| ExcelError::new(ExcelErrorKind::Value).with_message(message))?
6648        };
6649        replay_records.sort_by_key(|record| record.source_order);
6650        for chunk in replay_records.chunks(256) {
6651            self.target_preparation_checkpoint(deadline, chunk.len() as u64)?;
6652        }
6653        if replay_records
6654            .windows(2)
6655            .any(|records| records[0].source_order == records[1].source_order)
6656        {
6657            return Err(ExcelError::new(ExcelErrorKind::Value)
6658                .with_message("duplicate deferred source-order proof"));
6659        }
6660
6661        let disposition = replay_disposition;
6662        let direct_families = 0usize;
6663        let direct_cells = 0u64;
6664        let direct_fragments = 0u64;
6665        let direct_complete_families = 0u64;
6666        let direct_complete_cells = 0u64;
6667        let direct_partition_families = 0u64;
6668        let direct_partition_cells = 0u64;
6669        let anchor_parses = 0u64;
6670        let anchor_asts = 0u64;
6671        let anchor_analyses = 0u64;
6672
6673        Ok(PreparedTargetSourcePackage {
6674            sheet: sheet.to_string(),
6675            sheet_id,
6676            lease,
6677            selected_points: None,
6678            complete_selections: Default::default(),
6679            deferred_shared: false,
6680            direct_domains: Vec::new(),
6681            source_report,
6682            replay_records,
6683            spool_replays: 1,
6684            disposition,
6685            legacy: Vec::new(),
6686            direct_families,
6687            direct_cells,
6688            direct_fragments,
6689            direct_complete_families,
6690            direct_complete_cells,
6691            direct_partition_families,
6692            direct_partition_cells,
6693            anchor_parses,
6694            anchor_asts,
6695            anchor_analyses,
6696        })
6697    }
6698
6699    fn fallback_planning_snapshot(
6700        &self,
6701        batch: &FormulaIngestBatch,
6702    ) -> Result<crate::function_registry::RegistryPlanningSnapshot, ExcelError> {
6703        let mut requests = Vec::new();
6704        for formula in &batch.formulas {
6705            let ast = self
6706                .graph
6707                .data_store()
6708                .retrieve_ast(formula.ast_id, self.graph.sheet_reg())
6709                .ok_or_else(|| {
6710                    ExcelError::new(ExcelErrorKind::Value)
6711                        .with_message("ordered fallback AST is unavailable")
6712                })?;
6713            Self::collect_planning_function_requests(&ast, &mut requests);
6714        }
6715        requests.sort();
6716        requests.dedup();
6717        crate::function_registry::RegistryPlanningSnapshot::capture_for_requests(
6718            &self.resolver,
6719            requests,
6720        )
6721        .map_err(|error| ExcelError::new(ExcelErrorKind::Value).with_message(format!("{error:?}")))
6722    }
6723
6724    fn prepare_legacy_batch_fallback(
6725        &mut self,
6726        batch: FormulaIngestBatch,
6727        function_provider: &dyn crate::traits::FunctionProvider,
6728    ) -> Result<
6729        (
6730            crate::engine::graph::prepared_legacy_graph::PreparedLegacyGraphPlan,
6731            u64,
6732        ),
6733        ExcelError,
6734    > {
6735        let formula_count = batch.formulas.len() as u64;
6736        let sheet_id = self.graph.sheet_id_mut(&batch.sheet_name);
6737        let mut planned = Vec::with_capacity(batch.formulas.len());
6738        for record in batch.formulas {
6739            let placement = CellRef::new(
6740                sheet_id,
6741                Coord::from_excel(record.row, record.col, true, true),
6742            );
6743            let ingested = self
6744                .graph
6745                .ingest_pipeline(function_provider)
6746                .enable_function_semantics()
6747                .ingest_formula(
6748                    FormulaAstInput::RawArena(record.ast_id),
6749                    placement,
6750                    record.formula_text,
6751                )
6752                .map_err(|error| {
6753                    ExcelError::new(ExcelErrorKind::Value).with_message(format!("{error:?}"))
6754                })?;
6755            planned.push((record.row, record.col, ingested.ast_id, ingested.dep_plan));
6756        }
6757        let plan = self
6758            .graph
6759            .prepare_legacy_graph_plan(sheet_id, planned)
6760            .map_err(|error| {
6761                ExcelError::new(ExcelErrorKind::Value).with_message(error.to_string())
6762            })?;
6763        Ok((plan, formula_count))
6764    }
6765
6766    fn publish_compressed_partial_report(
6767        &mut self,
6768        report: &FormulaIngestReport,
6769        direct_report: &FormulaIngestReport,
6770    ) {
6771        if direct_report.source_family_promoted == 0
6772            && direct_report.graph_formula_cells_materialized == 0
6773        {
6774            return;
6775        }
6776        let mut published = report.clone();
6777        published.accumulate(direct_report);
6778        self.record_formula_ingest_report(published);
6779    }
6780
6781    fn finish_compressed_formula_sources(
6782        &mut self,
6783        batches: Vec<(
6784            FormulaIngestBatch,
6785            crate::engine::FormulaCompressedSourceReport,
6786            crate::engine::FormulaCompressedPreparation,
6787        )>,
6788    ) -> Result<FormulaIngestReport, ExcelError> {
6789        self.observe_function_semantic_epoch()?;
6790        if batches.iter().any(|(_, _, preparation)| {
6791            !Arc::ptr_eq(&preparation.engine_token, &self.source_formula_token)
6792        }) {
6793            return Err(ExcelError::new(ExcelErrorKind::Value)
6794                .with_message("compressed source preparation belongs to another engine"));
6795        }
6796        if batches.iter().any(|(fallback, _, preparation)| {
6797            preparation.sheet_name.as_ref() != fallback.sheet_name
6798        }) {
6799            return Err(ExcelError::new(ExcelErrorKind::Value)
6800                .with_message("compressed source preparation sheet mismatch"));
6801        }
6802        let initial_guard = crate::function_registry::semantic_epoch_read_guard();
6803        let initial_provider_revision = self.resolver.planning_semantic_revision();
6804        let mut fallback_batches = Vec::with_capacity(batches.len());
6805        let mut stale_fallback_batches = Vec::new();
6806        let mut pending_preparations = Vec::new();
6807        for (mut fallback, mut source, mut preparation) in batches {
6808            for formula in fallback.formulas.drain(..) {
6809                let source_order = formula.source_order.ok_or_else(|| {
6810                    ExcelError::new(ExcelErrorKind::Value).with_message(
6811                        "compressed source supplied formulas without source-order proof",
6812                    )
6813                })?;
6814                let text = formula.formula_text.ok_or_else(|| {
6815                    ExcelError::new(ExcelErrorKind::Value).with_message(
6816                        "ordered compressed fallback formula has no exact source text",
6817                    )
6818                })?;
6819                preparation
6820                    .eager_replay
6821                    .push(crate::engine::DeferredReplayFormula {
6822                        source_order,
6823                        row: formula.row,
6824                        col: formula.col,
6825                        text: text.to_string(),
6826                        family: formula.source_family,
6827                        partition_owner: formula.partition_owner,
6828                    });
6829            }
6830            preparation
6831                .eager_replay
6832                .sort_by_key(|record| record.source_order);
6833            source.source_spool_replays = source
6834                .source_spool_replays
6835                .saturating_add(preparation.preparation_spool_replays);
6836            let stale_reason = preparation
6837                .function_semantics_used
6838                .then(|| {
6839                    if preparation.function_provider_revision != initial_provider_revision {
6840                        Some("FunctionProviderRevisionChanged")
6841                    } else if self.prepared_function_semantics_changed(&preparation, &initial_guard)
6842                    {
6843                        Some("FunctionSemanticEpochChanged")
6844                    } else {
6845                        None
6846                    }
6847                })
6848                .flatten();
6849            let stale_semantics = stale_reason.is_some();
6850            for reason in preparation.rejected.values() {
6851                *source.fallback_reasons.entry(reason.clone()).or_default() += 1;
6852            }
6853            // No family is ever placed directly: every family replays.
6854            source.replay_families = source.families_seen;
6855            source.replay_cells = source.family_cells_seen;
6856            let compressed = crate::engine::FormulaCompressedSourceBatch::new(
6857                fallback.sheet_name.clone(),
6858                source,
6859            );
6860            if stale_semantics {
6861                stale_fallback_batches.push((fallback, compressed));
6862            } else {
6863                fallback_batches.push((fallback, compressed));
6864            }
6865            pending_preparations.push((preparation, stale_semantics));
6866        }
6867        drop(initial_guard);
6868
6869        // Build known fallback graphs first. Stale batches are forced through legacy ingest.
6870        let configured_mode = self.config.formula_plane_mode;
6871        self.config.formula_plane_mode = FormulaPlaneMode::Off;
6872        let stale_result =
6873            self.ingest_compressed_formula_source_batches_inner(stale_fallback_batches, false);
6874        self.config.formula_plane_mode = configured_mode;
6875        let mut report = stale_result?;
6876        report.mode = configured_mode;
6877
6878        self.config.formula_plane_mode = FormulaPlaneMode::Off;
6879        let fallback_result =
6880            self.ingest_compressed_formula_source_batches_inner(fallback_batches, false);
6881        self.config.formula_plane_mode = configured_mode;
6882        match fallback_result {
6883            Ok(fallback_report) => report.accumulate(&fallback_report),
6884            Err(error) => {
6885                self.record_formula_ingest_report(report);
6886                return Err(error);
6887            }
6888        }
6889
6890        let mut direct_report =
6891            FormulaIngestReport::with_mode(FormulaPlaneMode::AuthoritativeExperimental);
6892        for (preparation, _) in &pending_preparations {
6893            direct_report.source_anchor_parses = direct_report.source_anchor_parses.saturating_add(
6894                preparation.clean_rejected_anchor_counts[0]
6895                    .saturating_add(preparation.fragmented_rejected_anchor_counts[0]),
6896            );
6897            direct_report.source_anchor_asts = direct_report.source_anchor_asts.saturating_add(
6898                preparation.clean_rejected_anchor_counts[1]
6899                    .saturating_add(preparation.fragmented_rejected_anchor_counts[1]),
6900            );
6901            direct_report.source_anchor_analyses =
6902                direct_report.source_anchor_analyses.saturating_add(
6903                    preparation.clean_rejected_anchor_counts[2]
6904                        .saturating_add(preparation.fragmented_rejected_anchor_counts[2]),
6905                );
6906        }
6907        loop {
6908            #[cfg(any(test, feature = "test-support"))]
6909            if let Some(hook) = self.before_prepared_span_commit_hook.take() {
6910                hook();
6911            }
6912            let commit_guard = crate::function_registry::semantic_epoch_read_guard();
6913            let commit_provider_revision = self.resolver.planning_semantic_revision();
6914            let mut newly_stale = Vec::new();
6915            let mut current = Vec::new();
6916            for pending in pending_preparations.drain(..) {
6917                let stale_reason = pending
6918                    .0
6919                    .function_semantics_used
6920                    .then(|| {
6921                        if pending.0.function_provider_revision != commit_provider_revision {
6922                            Some("FunctionProviderRevisionChanged")
6923                        } else if self
6924                            .prepared_function_semantics_changed(&pending.0, &commit_guard)
6925                        {
6926                            Some("FunctionSemanticEpochChanged")
6927                        } else {
6928                            None
6929                        }
6930                    })
6931                    .flatten();
6932                if let Some(reason) = stale_reason {
6933                    newly_stale.push((pending, reason));
6934                } else {
6935                    current.push(pending);
6936                }
6937            }
6938            if !newly_stale.is_empty() {
6939                drop(commit_guard);
6940                for ((mut preparation, was_initially_stale), reason) in newly_stale {
6941                    preparation
6942                        .eager_replay
6943                        .sort_by_key(|record| record.source_order);
6944                    direct_report.source_spool_replays =
6945                        direct_report.source_spool_replays.saturating_add(1);
6946                    if !was_initially_stale {
6947                        direct_report
6948                            .fallback_reasons
6949                            .entry(reason.to_string())
6950                            .or_default();
6951                    }
6952                    preparation.function_semantics_used = false;
6953                    if !preparation.eager_replay.is_empty() {
6954                        current.push((preparation, false));
6955                    }
6956                }
6957                pending_preparations = current;
6958                continue;
6959            }
6960            drop(commit_guard);
6961
6962            // Replay fallback families in source order.
6963            enum SourceProposal {
6964                KnownFallback {
6965                    source_order: crate::engine::SourceFormulaOrder,
6966                    records: Vec<crate::engine::DeferredReplayFormula>,
6967                },
6968            }
6969
6970            impl SourceProposal {
6971                fn source_order(&self) -> crate::engine::SourceFormulaOrder {
6972                    match self {
6973                        Self::KnownFallback { source_order, .. } => *source_order,
6974                    }
6975                }
6976            }
6977
6978            for (mut preparation, _) in current {
6979                let mut proposals = Vec::with_capacity(preparation.eager_replay.len());
6980                let mut family_fallbacks: BTreeMap<_, Vec<_>> = BTreeMap::new();
6981                for record in preparation.eager_replay.drain(..) {
6982                    if let Some(owner) = record.partition_owner.or(record.family) {
6983                        family_fallbacks.entry(owner).or_default().push(record);
6984                    } else {
6985                        proposals.push(SourceProposal::KnownFallback {
6986                            source_order: record.source_order,
6987                            records: vec![record],
6988                        });
6989                    }
6990                }
6991                for (_, mut records) in family_fallbacks {
6992                    records.sort_by_key(|record| record.source_order);
6993                    if records
6994                        .windows(2)
6995                        .any(|window| window[0].source_order == window[1].source_order)
6996                    {
6997                        self.publish_compressed_partial_report(&report, &direct_report);
6998                        return Err(ExcelError::new(ExcelErrorKind::Value)
6999                            .with_message("duplicate exact-replay source-order proof"));
7000                    }
7001                    let Some(source_order) = records.first().map(|record| record.source_order)
7002                    else {
7003                        self.publish_compressed_partial_report(&report, &direct_report);
7004                        return Err(ExcelError::new(ExcelErrorKind::Value)
7005                            .with_message("empty exact-replay fallback family"));
7006                    };
7007                    proposals.push(SourceProposal::KnownFallback {
7008                        source_order,
7009                        records,
7010                    });
7011                }
7012                proposals.sort_by_key(SourceProposal::source_order);
7013                if proposals
7014                    .windows(2)
7015                    .any(|window| window[0].source_order() == window[1].source_order())
7016                {
7017                    self.publish_compressed_partial_report(&report, &direct_report);
7018                    return Err(ExcelError::new(ExcelErrorKind::Value)
7019                        .with_message("ambiguous compressed source-order proof"));
7020                }
7021
7022                for proposal in proposals {
7023                    match proposal {
7024                        SourceProposal::KnownFallback { records, .. } => {
7025                            let batch = match self.formula_batch_from_exact_replay(
7026                                preparation.sheet_name.as_ref(),
7027                                records,
7028                            ) {
7029                                Ok(batch) => batch,
7030                                Err(error) => {
7031                                    self.publish_compressed_partial_report(&report, &direct_report);
7032                                    return Err(error);
7033                                }
7034                            };
7035                            if batch.is_empty() {
7036                                continue;
7037                            }
7038                            let snapshot = match self.fallback_planning_snapshot(&batch) {
7039                                Ok(snapshot) => snapshot,
7040                                Err(error) => {
7041                                    self.publish_compressed_partial_report(&report, &direct_report);
7042                                    return Err(error);
7043                                }
7044                            };
7045                            let commit_guard =
7046                                crate::function_registry::semantic_epoch_read_guard();
7047                            let provider_revision_initial =
7048                                self.resolver.planning_semantic_revision();
7049                            if (commit_guard.epoch() != snapshot.epoch()
7050                                && snapshot.semantic_changes_affect_requests_since_guarded(
7051                                    &commit_guard,
7052                                    snapshot.epoch(),
7053                                ))
7054                                || snapshot.provider_revision().is_some_and(|revision| {
7055                                    Some(revision) != provider_revision_initial
7056                                })
7057                            {
7058                                drop(commit_guard);
7059                                self.publish_compressed_partial_report(&report, &direct_report);
7060                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
7061                                    "ordered fallback planning snapshot became stale",
7062                                ));
7063                            }
7064                            let (plan, formula_count) = match self
7065                                .prepare_legacy_batch_fallback(batch, &snapshot)
7066                            {
7067                                Ok(plan) => plan,
7068                                Err(error) => {
7069                                    drop(commit_guard);
7070                                    self.publish_compressed_partial_report(&report, &direct_report);
7071                                    return Err(error);
7072                                }
7073                            };
7074                            let provider_revision_after =
7075                                self.resolver.planning_semantic_revision();
7076                            if provider_revision_after != provider_revision_initial {
7077                                drop(commit_guard);
7078                                self.publish_compressed_partial_report(&report, &direct_report);
7079                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
7080                                    "function provider changed while preparing ordered fallback",
7081                                ));
7082                            }
7083                            let graph_vertices = plan.new_vertex_count();
7084                            let Some(graph_edges) = plan.planned_edge_count() else {
7085                                drop(commit_guard);
7086                                self.publish_compressed_partial_report(&report, &direct_report);
7087                                return Err(ExcelError::new(ExcelErrorKind::Value)
7088                                    .with_message("prepared ordered fallback size overflow"));
7089                            };
7090                            if let Err(error) = self.prepared_legacy_admission(&plan, formula_count)
7091                            {
7092                                drop(commit_guard);
7093                                self.publish_compressed_partial_report(&report, &direct_report);
7094                                return Err(error);
7095                            }
7096                            if let Err(error) =
7097                                self.graph.validate_prepared_legacy_graph_plan(&plan)
7098                            {
7099                                drop(commit_guard);
7100                                self.publish_compressed_partial_report(&report, &direct_report);
7101                                return Err(ExcelError::new(ExcelErrorKind::Value)
7102                                    .with_message(error.to_string()));
7103                            }
7104                            #[cfg(test)]
7105                            if let Some(hook) = self
7106                                .before_legacy_fallback_final_provider_sample_hook
7107                                .take()
7108                            {
7109                                hook();
7110                            }
7111                            let provider_revision_final =
7112                                self.resolver.planning_semantic_revision();
7113                            if provider_revision_final != provider_revision_initial {
7114                                drop(commit_guard);
7115                                self.publish_compressed_partial_report(&report, &direct_report);
7116                                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
7117                                    "function provider changed after ordered fallback validation",
7118                                ));
7119                            }
7120                            let graph_formulas =
7121                                self.graph.apply_prevalidated_legacy_graph_plan(plan);
7122                            direct_report.formula_cells_seen = direct_report
7123                                .formula_cells_seen
7124                                .saturating_add(formula_count);
7125                            direct_report.graph_formula_cells_materialized = direct_report
7126                                .graph_formula_cells_materialized
7127                                .saturating_add(graph_formulas as u64);
7128                            direct_report.graph_vertices_created = direct_report
7129                                .graph_vertices_created
7130                                .saturating_add(graph_vertices as u64);
7131                            direct_report.graph_edges_created = direct_report
7132                                .graph_edges_created
7133                                .saturating_add(graph_edges as u64);
7134                        }
7135                    }
7136                    #[cfg(test)]
7137                    if let Some(hook) = self.after_eager_proposal_commit_hook.take() {
7138                        hook();
7139                    }
7140                }
7141            }
7142            break;
7143        }
7144        report.accumulate(&direct_report);
7145        self.record_formula_ingest_report(report.clone());
7146        Ok(report)
7147    }
7148    /// Ingest replayed per-cell formulas while preserving compressed source counters.
7149    pub(crate) fn ingest_compressed_formula_source_batches(
7150        &mut self,
7151        batches: Vec<(
7152            FormulaIngestBatch,
7153            crate::engine::FormulaCompressedSourceBatch,
7154        )>,
7155    ) -> Result<FormulaIngestReport, ExcelError> {
7156        self.ingest_compressed_formula_source_batches_inner(batches, true)
7157    }
7158
7159    fn ingest_compressed_formula_source_batches_inner(
7160        &mut self,
7161        batches: Vec<(
7162            FormulaIngestBatch,
7163            crate::engine::FormulaCompressedSourceBatch,
7164        )>,
7165        publish_report: bool,
7166    ) -> Result<FormulaIngestReport, ExcelError> {
7167        let mut source_counts = [0_u64; 11];
7168        let mut source_report = crate::engine::FormulaCompressedSourceReport::default();
7169        let mut formula_batches = Vec::with_capacity(batches.len());
7170        let mut compressed_families = Vec::new();
7171        let mut partitioned_families = Vec::new();
7172        for (batch, compressed_batch) in batches {
7173            let (sheet_name, compressed, families, partitions) = compressed_batch.into_parts();
7174            if sheet_name.as_ref() != batch.sheet_name {
7175                return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
7176                    "compressed formula source sheet does not match its replay batch",
7177                ));
7178            }
7179            compressed_families.push((batch.sheet_name.clone(), families));
7180            partitioned_families.push((batch.sheet_name.clone(), partitions));
7181            source_counts[0] = source_counts[0].saturating_add(compressed.source_formula_events);
7182            source_counts[1] = source_counts[1].saturating_add(compressed.source_ordinary_events);
7183            source_counts[2] =
7184                source_counts[2].saturating_add(compressed.source_shared_anchor_events);
7185            source_counts[3] =
7186                source_counts[3].saturating_add(compressed.source_shared_descendant_events);
7187            source_counts[4] = source_counts[4].saturating_add(compressed.source_unknown_events);
7188            source_counts[5] =
7189                source_counts[5].saturating_add(compressed.source_formula_records_spooled);
7190            source_counts[6] =
7191                source_counts[6].saturating_add(compressed.source_spool_encoded_bytes);
7192            source_counts[7] = source_counts[7].max(compressed.source_spool_peak_memory_bytes);
7193            source_counts[8] =
7194                source_counts[8].saturating_add(compressed.source_spool_spilled_bytes);
7195            source_counts[9] = source_counts[9].saturating_add(compressed.source_spool_spill_files);
7196            source_counts[10] = source_counts[10].saturating_add(compressed.source_spool_replays);
7197            source_report.families_seen = source_report
7198                .families_seen
7199                .saturating_add(compressed.families_seen);
7200            source_report.family_cells_seen = source_report
7201                .family_cells_seen
7202                .saturating_add(compressed.family_cells_seen);
7203            source_report.source_clean_families = source_report
7204                .source_clean_families
7205                .saturating_add(compressed.source_clean_families);
7206            source_report.source_clean_cells = source_report
7207                .source_clean_cells
7208                .saturating_add(compressed.source_clean_cells);
7209            source_report.source_fragmentable_families = source_report
7210                .source_fragmentable_families
7211                .saturating_add(compressed.source_fragmentable_families);
7212            source_report.source_fragmentable_cells = source_report
7213                .source_fragmentable_cells
7214                .saturating_add(compressed.source_fragmentable_cells);
7215            source_report.source_fragment_count = source_report
7216                .source_fragment_count
7217                .saturating_add(compressed.source_fragment_count);
7218            source_report.source_isolated_fallback_cells = source_report
7219                .source_isolated_fallback_cells
7220                .saturating_add(compressed.source_isolated_fallback_cells);
7221            source_report.source_hole_exclusions = source_report
7222                .source_hole_exclusions
7223                .saturating_add(compressed.source_hole_exclusions);
7224            source_report.source_ordinary_exclusions = source_report
7225                .source_ordinary_exclusions
7226                .saturating_add(compressed.source_ordinary_exclusions);
7227            source_report.source_partition_failures = source_report
7228                .source_partition_failures
7229                .saturating_add(compressed.source_partition_failures);
7230            source_report.replay_families = source_report
7231                .replay_families
7232                .saturating_add(compressed.replay_families);
7233            source_report.replay_cells = source_report
7234                .replay_cells
7235                .saturating_add(compressed.replay_cells);
7236            source_report.forward_descendants = source_report
7237                .forward_descendants
7238                .saturating_add(compressed.forward_descendants);
7239            source_report.evidence_limit_fallbacks = source_report
7240                .evidence_limit_fallbacks
7241                .saturating_add(compressed.evidence_limit_fallbacks);
7242            source_report.evidence_peak_bytes = source_report
7243                .evidence_peak_bytes
7244                .max(compressed.evidence_peak_bytes);
7245            for (reason, count) in compressed.fallback_reasons {
7246                *source_report.fallback_reasons.entry(reason).or_default() += count;
7247            }
7248            formula_batches.push(batch);
7249        }
7250        self.ingest_formula_batches_inner(
7251            formula_batches,
7252            source_counts,
7253            Some(source_report),
7254            compressed_families,
7255            partitioned_families,
7256            publish_report,
7257        )
7258    }
7259
7260    pub fn ingest_formula_batches(
7261        &mut self,
7262        batches: Vec<FormulaIngestBatch>,
7263    ) -> Result<FormulaIngestReport, ExcelError> {
7264        let has_formulas = batches.iter().any(|batch| !batch.formulas.is_empty());
7265        let report = self.ingest_formula_batches_inner(
7266            batches,
7267            [0; 11],
7268            None,
7269            Vec::new(),
7270            Vec::new(),
7271            true,
7272        )?;
7273        if has_formulas {
7274            self.mark_topology_edited();
7275        }
7276        Ok(report)
7277    }
7278
7279    fn ingest_formula_batches_unpublished(
7280        &mut self,
7281        batches: Vec<FormulaIngestBatch>,
7282    ) -> Result<FormulaIngestReport, ExcelError> {
7283        self.ingest_formula_batches_inner(batches, [0; 11], None, Vec::new(), Vec::new(), false)
7284    }
7285
7286    fn ingest_formula_batches_inner(
7287        &mut self,
7288        batches: Vec<FormulaIngestBatch>,
7289        source_counts: [u64; 11],
7290        source_report: Option<crate::engine::FormulaCompressedSourceReport>,
7291        compressed_families: Vec<(String, Vec<crate::engine::SourceFormulaFamily>)>,
7292        partitioned_families: Vec<(String, Vec<crate::engine::PartitionedSourceFormulaFamily>)>,
7293        publish_report: bool,
7294    ) -> Result<FormulaIngestReport, ExcelError> {
7295        self.observe_function_semantic_epoch()?;
7296        let formula_cells_seen = batches.iter().map(|batch| batch.len() as u64).sum();
7297        #[cfg(feature = "tracing")]
7298        let arena_nodes_before = self.graph.data_store().memory_usage().total_ast_nodes;
7299        #[cfg(feature = "tracing")]
7300        let route = if !partitioned_families.is_empty() {
7301            "partitioned"
7302        } else if !compressed_families.is_empty() {
7303            "compressed_source"
7304        } else if source_report.is_some() {
7305            "replay"
7306        } else {
7307            "ordinary"
7308        };
7309        let _ingest_span = crate::engine::trace::fz_span!(
7310            tracing::Level::INFO,
7311            "ingest",
7312            "ingest.batch",
7313            mode = ?FormulaPlaneMode::Off,
7314            route,
7315            formula_cells = formula_cells_seen
7316        );
7317        let mut report = FormulaIngestReport::with_mode(FormulaPlaneMode::Off);
7318        let materialize_batches = batches;
7319        report.formula_cells_seen = formula_cells_seen;
7320        report.source_formula_events = source_counts[0];
7321        report.source_ordinary_events = source_counts[1];
7322        report.source_shared_anchor_events = source_counts[2];
7323        report.source_shared_descendant_events = source_counts[3];
7324        report.source_unknown_events = source_counts[4];
7325        report.source_formula_records_spooled = source_counts[5];
7326        report.source_spool_encoded_bytes = source_counts[6];
7327        report.source_spool_peak_memory_bytes = source_counts[7];
7328        report.source_spool_spilled_bytes = source_counts[8];
7329        report.source_spool_spill_files = source_counts[9];
7330        report.source_spool_replays = source_counts[10];
7331        if let Some(source) = source_report {
7332            report.source_families_seen = source.families_seen;
7333            report.source_family_cells_seen = source.family_cells_seen;
7334            report.source_family_shadow_eligible = source.source_clean_families;
7335            report.source_family_shadow_eligible_cells = source.source_clean_cells;
7336            report.source_partitioned_families_seen = source.source_fragmentable_families;
7337            report.source_partition_holes = source.source_hole_exclusions;
7338            report.source_partition_ordinary_exceptions = source.source_ordinary_exclusions;
7339            report.source_partition_failures = source.source_partition_failures;
7340            report.source_partition_surviving_cells = source.source_fragmentable_cells;
7341            report.source_family_fallback = report
7342                .source_family_fallback
7343                .saturating_add(source.replay_families);
7344            report.source_family_fallback_cells = report
7345                .source_family_fallback_cells
7346                .saturating_add(source.replay_cells);
7347            report.source_forward_descendants = source.forward_descendants;
7348            report.source_evidence_limit_fallbacks = source.evidence_limit_fallbacks;
7349            report.source_evidence_peak_bytes = source.evidence_peak_bytes;
7350            for (reason, count) in source.fallback_reasons {
7351                let total = report.fallback_reasons.entry(reason).or_default();
7352                *total = total.saturating_add(count);
7353            }
7354        }
7355
7356        // A first load without graph admission plans in the builder, one
7357        // chunk at a time (a load error fails the load, so the builder's
7358        // partial application on a planning error is unobservable).
7359        if self.graph.first_load_assume_new()
7360            && !self.graph_admission_enabled()
7361            && !materialize_batches.iter().all(FormulaIngestBatch::is_empty)
7362        {
7363            let mut builder =
7364                crate::engine::ingest_builder::BulkIngestBuilder::new(&mut self.graph);
7365            for batch in materialize_batches {
7366                if batch.is_empty() {
7367                    continue;
7368                }
7369                let sheet_id = builder
7370                    .add_sheet_checked(&batch.sheet_name)
7371                    .ok_or_else(|| {
7372                        ExcelError::new(ExcelErrorKind::Ref)
7373                            .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
7374                    })?;
7375                builder.add_formula_refs(
7376                    sheet_id,
7377                    batch.formulas.into_iter().map(|record| {
7378                        (
7379                            record.row,
7380                            record.col,
7381                            crate::engine::graph::FormulaRef::of_ingested(
7382                                record.ast_id,
7383                                record.member_anchor,
7384                            ),
7385                        )
7386                    }),
7387                );
7388            }
7389            let summary = builder.finish_with_provider(&self.resolver)?;
7390            report.graph_formula_cells_materialized = summary.formulas as u64;
7391            report.graph_vertices_created = summary.vertices as u64;
7392            report.graph_edges_created = summary.edges as u64;
7393        } else if !materialize_batches.iter().all(FormulaIngestBatch::is_empty) {
7394            let mut prepared_by_sheet: BTreeMap<String, Vec<_>> = BTreeMap::new();
7395            for batch in materialize_batches {
7396                if batch.is_empty() {
7397                    continue;
7398                }
7399                let sheet_id = self.graph.sheet_id(&batch.sheet_name).ok_or_else(|| {
7400                    ExcelError::new(ExcelErrorKind::Ref)
7401                        .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
7402                })?;
7403                let mut pipeline = self.ingest_pipeline();
7404                // Plan each record and keep only what the graph needs (the
7405                // pipeline's per-formula facts are dropped at once).
7406                let prepared = prepared_by_sheet.entry(batch.sheet_name).or_default();
7407                prepared.reserve(batch.formulas.len());
7408                for record in batch.formulas {
7409                    let placement = CellRef::new(
7410                        sheet_id,
7411                        Coord::from_excel(record.row, record.col, true, true),
7412                    );
7413                    let input = match record.member_anchor {
7414                        Some(anchor) => FormulaAstInput::Member {
7415                            template: record.ast_id,
7416                            anchor,
7417                        },
7418                        None => FormulaAstInput::RawArena(record.ast_id),
7419                    };
7420                    let formula = pipeline.ingest_formula(input, placement, None)?;
7421                    prepared.push((
7422                        record.row,
7423                        record.col,
7424                        crate::engine::graph::FormulaRef::of_ingested(
7425                            formula.ast_id,
7426                            formula.member_anchor,
7427                        ),
7428                        formula.dep_plan,
7429                    ));
7430                }
7431            }
7432            let admission_preflighted = self.graph_admission_enabled();
7433            if admission_preflighted {
7434                let mut preview = Vec::new();
7435                for (sheet_name, formulas) in &prepared_by_sheet {
7436                    let sheet_id = self.graph.sheet_id(sheet_name).ok_or_else(|| {
7437                        ExcelError::new(ExcelErrorKind::Ref)
7438                            .with_message(format!("unknown ingest sheet: {sheet_name}"))
7439                    })?;
7440                    preview.extend(
7441                        formulas
7442                            .iter()
7443                            .map(|(row, col, _, plan)| (sheet_id, *row, *col, plan.clone())),
7444                    );
7445                }
7446                let admission = self.graph.preview_formula_mutations(&preview)?;
7447                self.preflight_graph_admission(admission)?;
7448            }
7449
7450            let mut builder = self.begin_bulk_ingest();
7451            if admission_preflighted {
7452                builder.mark_admission_preflighted();
7453            }
7454            for (sheet_name, formulas) in prepared_by_sheet {
7455                if formulas.is_empty() {
7456                    continue;
7457                }
7458                let sheet_id = builder.add_sheet(&sheet_name);
7459                builder.add_formula_plans(sheet_id, formulas);
7460            }
7461            let summary = builder.finish()?;
7462            report.graph_formula_cells_materialized = summary.formulas as u64;
7463            report.graph_vertices_created = summary.vertices as u64;
7464            report.graph_edges_created = summary.edges as u64;
7465        }
7466
7467        crate::engine::trace::fz_event!(
7468            tracing::Level::INFO,
7469            "ingest",
7470            "ingest.summary",
7471            candidate_cells = report.shadow_candidate_cells,
7472            accepted_span_cells = report.shadow_accepted_span_cells,
7473            fallback_cells = report.shadow_fallback_cells,
7474            spans_created = report.shadow_spans_created,
7475            templates_interned = report.shadow_templates_interned,
7476            graph_vertices_created = report.graph_vertices_created,
7477            graph_edges_created = report.graph_edges_created,
7478            arena_nodes_delta = self
7479                .graph
7480                .data_store()
7481                .memory_usage()
7482                .total_ast_nodes
7483                .saturating_sub(arena_nodes_before)
7484        );
7485        if publish_report {
7486            self.record_formula_ingest_report(report.clone());
7487        }
7488        Ok(report)
7489    }
7490
7491    fn dedup_formula_parse_diagnostics_since(&mut self, start: usize) {
7492        let mut unique = Vec::new();
7493        for diagnostic in self.formula_parse_diagnostics.drain(start..) {
7494            let duplicate = unique.iter().any(|prior: &FormulaParseDiagnostic| {
7495                prior.sheet == diagnostic.sheet
7496                    && prior.row == diagnostic.row
7497                    && prior.col == diagnostic.col
7498                    && prior.formula == diagnostic.formula
7499                    && prior.policy == diagnostic.policy
7500            });
7501            if !duplicate {
7502                unique.push(diagnostic);
7503            }
7504        }
7505        self.formula_parse_diagnostics.extend(unique);
7506    }
7507
7508    pub fn handle_formula_parse_error(
7509        &mut self,
7510        sheet: &str,
7511        row: u32,
7512        col: u32,
7513        formula: &str,
7514        message: String,
7515    ) -> Result<Option<ASTNode>, ExcelError> {
7516        let policy = self.config.formula_parse_policy;
7517
7518        if policy == FormulaParsePolicy::Strict {
7519            let col_a1 = col_letters_from_1based(col).unwrap_or_else(|_| "?".to_string());
7520            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(format!(
7521                "Formula parse error at {sheet}!{col_a1}{row}: {message}"
7522            )));
7523        }
7524
7525        self.formula_parse_diagnostics.push(FormulaParseDiagnostic {
7526            sheet: sheet.to_string(),
7527            row,
7528            col,
7529            formula: formula.to_string(),
7530            message: message.clone(),
7531            policy,
7532        });
7533
7534        match policy {
7535            FormulaParsePolicy::Strict => unreachable!(),
7536            FormulaParsePolicy::KeepCachedValue => Ok(None),
7537            FormulaParsePolicy::AsText => Ok(Some(ASTNode::new(
7538                ASTNodeType::Literal(LiteralValue::Text(formula.to_string())),
7539                None,
7540            ))),
7541            FormulaParsePolicy::CoerceToError => {
7542                let err = ExcelError::new(ExcelErrorKind::Error)
7543                    .with_message(format!("Malformed formula: {message}"));
7544                Ok(Some(ASTNode::new(
7545                    ASTNodeType::Literal(LiteralValue::Error(err)),
7546                    None,
7547                )))
7548            }
7549        }
7550    }
7551
7552    #[cfg(test)]
7553    fn target_preparation_fault(
7554        &mut self,
7555        seam: crate::engine::target_preparation::TargetPreparationFault,
7556    ) -> Result<(), ExcelError> {
7557        if self.target_preparation_fault_for_test == Some(seam) {
7558            self.target_preparation_fault_for_test = None;
7559            Err(ExcelError::new(ExcelErrorKind::Value)
7560                .with_message(format!("injected target preparation fault: {seam:?}")))
7561        } else {
7562            Ok(())
7563        }
7564    }
7565
7566    fn preparation_stale(
7567        reason: formualizer_common::PreparationStaleReason,
7568        message: impl Into<String>,
7569    ) -> ExcelError {
7570        ExcelError::new(ExcelErrorKind::Value)
7571            .with_message(message)
7572            .with_extra(formualizer_common::ExcelErrorExtra::PreparationStale { reason })
7573    }
7574
7575    fn preparation_revision_stale_reason(
7576        assumptions: &crate::engine::PreparationRevision,
7577        current: &crate::engine::PreparationRevision,
7578        planning_requests: &BTreeSet<(String, String, usize)>,
7579        staged_leases_match: bool,
7580    ) -> Option<formualizer_common::PreparationStaleReason> {
7581        if assumptions.graph != current.graph {
7582            Some(formualizer_common::PreparationStaleReason::Graph)
7583        } else if assumptions.staged != current.staged || !staged_leases_match {
7584            Some(formualizer_common::PreparationStaleReason::Staged)
7585        } else if assumptions.symbols != current.symbols {
7586            Some(formualizer_common::PreparationStaleReason::Symbols)
7587        } else if assumptions.provider != current.provider {
7588            Some(formualizer_common::PreparationStaleReason::Provider)
7589        } else if assumptions.semantic != current.semantic
7590            && crate::function_registry::semantic_changes_affect_requests_since(
7591                assumptions.semantic,
7592                planning_requests.iter().cloned(),
7593            )
7594        {
7595            Some(formualizer_common::PreparationStaleReason::Semantic)
7596        } else {
7597            None
7598        }
7599    }
7600
7601    fn preparation_revisions(&self) -> crate::engine::PreparationRevision {
7602        crate::engine::PreparationRevision {
7603            graph: self.graph.topology_revision(),
7604            authority: 0,
7605            authority_indexes: 0,
7606            authority_indexed_plane: 0,
7607            staged: self.staged_formula_index.revision(),
7608            symbols: self.graph.symbol_revision(),
7609            semantic: crate::function_registry::semantic_epoch(),
7610            provider: self.resolver.planning_semantic_revision(),
7611        }
7612    }
7613
7614    fn planning_revision_snapshot(&self) -> PlanningRevisionSnapshot {
7615        let registry_guard = crate::function_registry::semantic_epoch_read_guard();
7616        let provider = self.resolver.planning_semantic_revision();
7617        let semantic = registry_guard.epoch();
7618        PlanningRevisionSnapshot {
7619            engine_topology_epoch: self.topology_epoch,
7620            graph_topology_revision: self.graph.topology_revision(),
7621            staged: self.staged_formula_index.revision(),
7622            symbols: self.graph.symbol_revision(),
7623            semantic,
7624            provider,
7625            deterministic_mode: self.config.deterministic_mode.clone(),
7626            budgets: self.evaluation_resource_budgets.clone(),
7627        }
7628    }
7629
7630    fn recalc_plan_key(&self) -> RecalcPlanKey {
7631        RecalcPlanKey {
7632            engine_token: Arc::clone(&self.recalc_plan_token),
7633            revisions: self.planning_revision_snapshot(),
7634        }
7635    }
7636
7637    fn plan_stale(reason: formualizer_common::PlanStaleReason) -> ExcelError {
7638        ExcelError::new(ExcelErrorKind::Value)
7639            .with_message(format!("recalculation plan is stale: {}", reason.as_str()))
7640            .with_extra(formualizer_common::ExcelErrorExtra::PlanStale { reason })
7641    }
7642
7643    fn validate_recalc_plan_key(&self, key: &RecalcPlanKey) -> Result<(), ExcelError> {
7644        use formualizer_common::PlanStaleReason;
7645
7646        if !Arc::ptr_eq(&key.engine_token, &self.recalc_plan_token) {
7647            return Err(Self::plan_stale(PlanStaleReason::Engine));
7648        }
7649
7650        let current = self.planning_revision_snapshot();
7651        let expected = &key.revisions;
7652        let stale = if expected.provider != current.provider {
7653            Some(PlanStaleReason::Provider)
7654        } else if expected.semantic != current.semantic {
7655            Some(PlanStaleReason::Semantic)
7656        } else if expected.budgets != current.budgets
7657            || expected.deterministic_mode != current.deterministic_mode
7658        {
7659            Some(PlanStaleReason::Budget)
7660        } else if expected.staged != current.staged {
7661            Some(PlanStaleReason::Staged)
7662        } else if expected.symbols != current.symbols {
7663            Some(PlanStaleReason::Symbols)
7664        } else if expected.graph_topology_revision != current.graph_topology_revision
7665            || expected.engine_topology_epoch != current.engine_topology_epoch
7666        {
7667            Some(PlanStaleReason::Graph)
7668        } else {
7669            None
7670        };
7671        stale.map_or(Ok(()), |reason| Err(Self::plan_stale(reason)))
7672    }
7673
7674    fn target_preparation_checkpoint(
7675        &mut self,
7676        deadline: Option<std::time::Instant>,
7677        work: u64,
7678    ) -> Result<(), ExcelError> {
7679        if self
7680            .active_cancel_flag
7681            .as_ref()
7682            .is_some_and(|cancel| cancel.is_cancelled())
7683        {
7684            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
7685                .with_message("target graph preparation cancelled"));
7686        }
7687        if deadline.is_some_and(|deadline| std::time::Instant::now() >= deadline) {
7688            return Err(crate::engine::ResourceLedgerError::Exhausted(
7689                formualizer_common::ResourceExhaustionDetail {
7690                    reason: formualizer_common::ResourceExhaustionReason::Deadline,
7691                    limit: 0,
7692                    observed: 1,
7693                    request_id: self
7694                        .active_evaluation_resource_request
7695                        .as_ref()
7696                        .map(|stats| stats.request_id),
7697                },
7698            )
7699            .into_excel_error());
7700        }
7701        self.charge_bounded_work(work)
7702    }
7703
7704    fn opaque_reason_in_ast(
7705        &self,
7706        ast: &ASTNode,
7707        provider: &dyn crate::traits::FunctionProvider,
7708    ) -> Option<crate::engine::OpaqueReason> {
7709        match &ast.node_type {
7710            ASTNodeType::Function { name, args } => {
7711                let canonical = name.rsplit('.').next().unwrap_or(name).to_ascii_uppercase();
7712                if canonical == "INDIRECT" {
7713                    return Some(crate::engine::OpaqueReason::RuntimeTextReference);
7714                }
7715                let Some(function) = provider.get_function_for_planning("", name) else {
7716                    return Some(crate::engine::OpaqueReason::UnknownFunction);
7717                };
7718                let caps = function.caps();
7719                if caps.contains(FnCaps::DYNAMIC_DEPENDENCY)
7720                    || caps.contains(FnCaps::RETURNS_REFERENCE)
7721                {
7722                    return Some(crate::engine::OpaqueReason::DynamicReference);
7723                }
7724                args.iter()
7725                    .find_map(|arg| self.opaque_reason_in_ast(arg, provider))
7726            }
7727            ASTNodeType::Call { .. } => Some(crate::engine::OpaqueReason::UnknownCustomFunction),
7728            ASTNodeType::UnaryOp { expr, .. } => self.opaque_reason_in_ast(expr, provider),
7729            ASTNodeType::BinaryOp { left, right, .. } => self
7730                .opaque_reason_in_ast(left, provider)
7731                .or_else(|| self.opaque_reason_in_ast(right, provider)),
7732            ASTNodeType::Array(rows) => rows
7733                .iter()
7734                .flat_map(|row| row.iter())
7735                .find_map(|item| self.opaque_reason_in_ast(item, provider)),
7736            ASTNodeType::Reference {
7737                reference:
7738                    ReferenceType::Cell {
7739                        sheet: Some(sheet), ..
7740                    }
7741                    | ReferenceType::Range {
7742                        sheet: Some(sheet), ..
7743                    },
7744                ..
7745            } if self.graph.sheet_id(sheet).is_none() => {
7746                Some(crate::engine::OpaqueReason::UnresolvedCrossSheetBinding)
7747            }
7748            ASTNodeType::Reference {
7749                reference:
7750                    ReferenceType::External(_)
7751                    | ReferenceType::Cell3D { .. }
7752                    | ReferenceType::Range3D { .. },
7753                ..
7754            } => Some(crate::engine::OpaqueReason::UnresolvedCrossSheetBinding),
7755            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => None,
7756        }
7757    }
7758
7759    fn target_planning_snapshot(
7760        &mut self,
7761        ast: &ASTNode,
7762        planning_requests: &mut BTreeSet<(String, String, usize)>,
7763    ) -> Result<crate::function_registry::RegistryPlanningSnapshot, ExcelError> {
7764        #[cfg(test)]
7765        if std::mem::take(&mut self.inject_target_semantic_stale_once_for_test) {
7766            return Err(Self::preparation_stale(
7767                formualizer_common::PreparationStaleReason::Semantic,
7768                "injected target semantic preparation movement",
7769            ));
7770        }
7771        #[cfg(test)]
7772        if let Some(hook) = self.before_target_planning_snapshot_hook.take() {
7773            hook();
7774        }
7775        let mut requests = Vec::new();
7776        Self::collect_planning_function_requests(ast, &mut requests);
7777        requests.sort();
7778        requests.dedup();
7779        planning_requests.extend(requests.iter().cloned());
7780        crate::function_registry::RegistryPlanningSnapshot::capture_for_requests(
7781            &self.resolver,
7782            requests,
7783        )
7784        .map_err(|error| {
7785            ExcelError::new(ExcelErrorKind::Value)
7786                .with_message(format!("target planning snapshot unavailable: {error:?}"))
7787        })
7788    }
7789
7790    fn target_planning_snapshot_stale_reason(
7791        snapshot: &crate::function_registry::RegistryPlanningSnapshot,
7792        assumptions: &crate::engine::PreparationRevision,
7793    ) -> Option<formualizer_common::PreparationStaleReason> {
7794        if snapshot
7795            .provider_revision()
7796            .is_some_and(|revision| Some(revision) != assumptions.provider)
7797        {
7798            Some(formualizer_common::PreparationStaleReason::Provider)
7799        } else if snapshot.epoch() != assumptions.semantic
7800            && snapshot.semantic_changes_affect_requests_since(assumptions.semantic)
7801        {
7802            Some(formualizer_common::PreparationStaleReason::Semantic)
7803        } else {
7804            None
7805        }
7806    }
7807
7808    fn widen_target_preparation(
7809        policy: crate::engine::OpaquePreparePolicy,
7810        scope: &mut crate::engine::PrepareScope,
7811        reasons: &mut Vec<crate::engine::OpaqueReason>,
7812        reason: crate::engine::OpaqueReason,
7813    ) -> Result<bool, ExcelError> {
7814        if policy == crate::engine::OpaquePreparePolicy::Error {
7815            return Err(ExcelError::new(ExcelErrorKind::NImpl)
7816                .with_message(format!("opaque target preparation semantics: {reason:?}")));
7817        }
7818        if !reasons.contains(&reason) {
7819            reasons.push(reason);
7820        }
7821        if !matches!(scope, crate::engine::PrepareScope::Workbook) {
7822            *scope = crate::engine::PrepareScope::Workbook;
7823            Ok(true)
7824        } else {
7825            Ok(false)
7826        }
7827    }
7828
7829    fn widen_target_preparation_to_sheet(
7830        policy: crate::engine::OpaquePreparePolicy,
7831        scope: &mut crate::engine::PrepareScope,
7832        reasons: &mut Vec<crate::engine::OpaqueReason>,
7833        reason: crate::engine::OpaqueReason,
7834        sheet: &str,
7835    ) -> Result<bool, ExcelError> {
7836        if policy == crate::engine::OpaquePreparePolicy::Error {
7837            return Err(ExcelError::new(ExcelErrorKind::NImpl)
7838                .with_message(format!("opaque target preparation semantics: {reason:?}")));
7839        }
7840        if !reasons.contains(&reason) {
7841            reasons.push(reason);
7842        }
7843        match scope {
7844            crate::engine::PrepareScope::Exact => {
7845                *scope = crate::engine::PrepareScope::Sheets(vec![sheet.to_string()]);
7846                Ok(true)
7847            }
7848            crate::engine::PrepareScope::Sheets(sheets) => {
7849                if sheets.iter().any(|candidate| candidate == sheet) {
7850                    Ok(false)
7851                } else {
7852                    sheets.push(sheet.to_string());
7853                    sheets.sort();
7854                    Ok(true)
7855                }
7856            }
7857            crate::engine::PrepareScope::Workbook => Ok(false),
7858        }
7859    }
7860
7861    fn ast_has_proven_sheet_local_dynamic(
7862        ast: &ASTNode,
7863        provider: &dyn crate::traits::FunctionProvider,
7864    ) -> bool {
7865        fn classify(ast: &ASTNode, provider: &dyn crate::traits::FunctionProvider) -> (bool, bool) {
7866            match &ast.node_type {
7867                ASTNodeType::Function { name, args } => {
7868                    let Some(function) = provider.get_function_for_planning("", name) else {
7869                        return (false, false);
7870                    };
7871                    let caps = function.caps();
7872                    let dynamic = caps.contains(FnCaps::DYNAMIC_DEPENDENCY)
7873                        || caps.contains(FnCaps::RETURNS_REFERENCE);
7874                    let canonical = name.rsplit('.').next().unwrap_or(name);
7875                    if dynamic
7876                        && !canonical.eq_ignore_ascii_case("OFFSET")
7877                        && !canonical.eq_ignore_ascii_case("INDEX")
7878                    {
7879                        return (false, true);
7880                    }
7881                    let mut has_dynamic = dynamic;
7882                    for arg in args {
7883                        let (safe, child_dynamic) = classify(arg, provider);
7884                        if !safe {
7885                            return (false, has_dynamic || child_dynamic);
7886                        }
7887                        has_dynamic |= child_dynamic;
7888                    }
7889                    (true, has_dynamic)
7890                }
7891                ASTNodeType::UnaryOp { expr, .. } => classify(expr, provider),
7892                ASTNodeType::BinaryOp { left, right, .. } => {
7893                    let (left_safe, left_dynamic) = classify(left, provider);
7894                    let (right_safe, right_dynamic) = classify(right, provider);
7895                    (left_safe && right_safe, left_dynamic || right_dynamic)
7896                }
7897                ASTNodeType::Array(rows) => {
7898                    let mut has_dynamic = false;
7899                    for item in rows.iter().flatten() {
7900                        let (safe, child_dynamic) = classify(item, provider);
7901                        if !safe {
7902                            return (false, has_dynamic || child_dynamic);
7903                        }
7904                        has_dynamic |= child_dynamic;
7905                    }
7906                    (true, has_dynamic)
7907                }
7908                ASTNodeType::Reference {
7909                    reference:
7910                        ReferenceType::Cell { sheet: None, .. }
7911                        | ReferenceType::Range { sheet: None, .. },
7912                    ..
7913                }
7914                | ASTNodeType::Literal(_)
7915                | ASTNodeType::Omitted => (true, false),
7916                ASTNodeType::Call { .. } | ASTNodeType::Reference { .. } => (false, false),
7917            }
7918        }
7919
7920        let (safe, dynamic) = classify(ast, provider);
7921        safe && dynamic
7922    }
7923
7924    fn table_selection_region(
7925        &self,
7926        entry: &crate::engine::graph::TableEntry,
7927        selection: &crate::engine::TableSelection,
7928    ) -> Result<PreparationRegion, ExcelError> {
7929        let mut start_row = entry.range.start.coord.row() + 1;
7930        let mut end_row = entry.range.end.coord.row() + 1;
7931        let mut start_col = entry.range.start.coord.col() + 1;
7932        let mut end_col = entry.range.end.coord.col() + 1;
7933        match selection {
7934            crate::engine::TableSelection::Whole => {}
7935            crate::engine::TableSelection::Headers => {
7936                if !entry.header_row {
7937                    return Err(ExcelError::new(ExcelErrorKind::Value)
7938                        .with_message(format!("table {} has no header row", entry.name)));
7939                }
7940                end_row = start_row;
7941            }
7942            crate::engine::TableSelection::Data => {
7943                if entry.header_row {
7944                    start_row = start_row.saturating_add(1);
7945                }
7946                if entry.totals_row {
7947                    end_row = end_row.saturating_sub(1);
7948                }
7949            }
7950            crate::engine::TableSelection::Totals => {
7951                if !entry.totals_row {
7952                    return Err(ExcelError::new(ExcelErrorKind::Value)
7953                        .with_message(format!("table {} has no totals row", entry.name)));
7954                }
7955                start_row = end_row;
7956            }
7957            crate::engine::TableSelection::Column(column) => {
7958                let index = entry.col_index(column).ok_or_else(|| {
7959                    ExcelError::new(ExcelErrorKind::Name)
7960                        .with_message(format!("unknown table column: {column}"))
7961                })?;
7962                start_col = start_col.saturating_add(index as u32);
7963                end_col = start_col;
7964            }
7965            crate::engine::TableSelection::Columns { start, end } => {
7966                let first = entry.col_index(start).ok_or_else(|| {
7967                    ExcelError::new(ExcelErrorKind::Name)
7968                        .with_message(format!("unknown table column: {start}"))
7969                })?;
7970                let last = entry.col_index(end).ok_or_else(|| {
7971                    ExcelError::new(ExcelErrorKind::Name)
7972                        .with_message(format!("unknown table column: {end}"))
7973                })?;
7974                if first > last {
7975                    return Err(ExcelError::new(ExcelErrorKind::Value)
7976                        .with_message("table column selection is reversed"));
7977                }
7978                start_col = start_col.saturating_add(first as u32);
7979                end_col = entry
7980                    .range
7981                    .start
7982                    .coord
7983                    .col()
7984                    .saturating_add(last as u32)
7985                    .saturating_add(1);
7986            }
7987        }
7988        if start_row > end_row && matches!(selection, crate::engine::TableSelection::Data) {
7989            start_row = entry.range.start.coord.row() + 1;
7990            end_row = start_row;
7991        }
7992        if start_row > end_row || start_col > end_col {
7993            return Err(
7994                ExcelError::new(ExcelErrorKind::Value).with_message("table selection is empty")
7995            );
7996        }
7997        Ok(PreparationRegion {
7998            sheet: self.graph.sheet_name(entry.sheet_id()).to_string(),
7999            sheet_id: entry.sheet_id(),
8000            start_row,
8001            start_col,
8002            end_row,
8003            end_col,
8004        })
8005    }
8006
8007    #[cfg(test)]
8008    pub(crate) fn reset_target_root_dedup_probes_for_test() {
8009        TARGET_ROOT_DEDUP_PROBES.with(|probes| probes.set(0));
8010    }
8011
8012    #[cfg(test)]
8013    pub(crate) fn target_root_dedup_probes_for_test() -> usize {
8014        TARGET_ROOT_DEDUP_PROBES.with(std::cell::Cell::get)
8015    }
8016
8017    pub(crate) fn resolve_target_producers(
8018        &mut self,
8019        targets: &[crate::engine::EvaluationTarget],
8020    ) -> Result<Vec<crate::engine::target_preparation::TargetProducer>, ExcelError> {
8021        use crate::engine::target_preparation::TargetProducer;
8022
8023        let request_id = self
8024            .active_evaluation_resource_request
8025            .as_ref()
8026            .map(|request| request.request_id);
8027        let mut roots = OrderedTargetProducers::with_capacity(targets.len())
8028            .map_err(|_| target_root_allocation_error(targets.len(), request_id))?;
8029        let resolve_region = |engine: &mut Self,
8030                              region: Region,
8031                              value_only: Option<CellRef>,
8032                              roots: &mut OrderedTargetProducers|
8033         -> Result<(), ExcelError> {
8034            let before = roots.len();
8035            for anchor in engine.graph.spill_anchors_in_region(
8036                region.sheet_id(),
8037                region.axis_ranges().0.query_bounds().0,
8038                region.axis_ranges().1.query_bounds().0,
8039                region.axis_ranges().0.query_bounds().1,
8040                region.axis_ranges().1.query_bounds().1,
8041            ) {
8042                roots
8043                    .push(TargetProducer::Legacy(anchor))
8044                    .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
8045            }
8046            for vertex in engine.graph.vertices_in_region(
8047                region.sheet_id(),
8048                region.axis_ranges().0.query_bounds().0,
8049                region.axis_ranges().0.query_bounds().1,
8050                region.axis_ranges().1.query_bounds().0,
8051                region.axis_ranges().1.query_bounds().1,
8052            ) {
8053                let vertex = engine
8054                    .graph
8055                    .get_cell_ref(vertex)
8056                    .and_then(|cell| engine.graph.spill_registry_anchor_for_cell(cell))
8057                    .unwrap_or(vertex);
8058                // `vertices_in_region` is a sheet-index query and a sheet index holds only
8059                // grid-addressed vertices, so a region can never yield a symbol: names,
8060                // tables and external sources have no position for a region to cover.
8061                // Symbol roots come from the by-name lookups below instead.
8062                if matches!(
8063                    engine.graph.get_vertex_kind(vertex),
8064                    VertexKind::FormulaScalar | VertexKind::FormulaArray
8065                ) {
8066                    roots
8067                        .push(TargetProducer::Legacy(vertex))
8068                        .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
8069                }
8070            }
8071            if roots.len() == before
8072                && let Some(cell) = value_only
8073            {
8074                roots
8075                    .push(TargetProducer::ValueOnly(cell))
8076                    .map_err(|_| target_root_allocation_error(roots.len() + 1, request_id))?;
8077            }
8078            Ok(())
8079        };
8080
8081        for target in targets {
8082            match target {
8083                crate::engine::EvaluationTarget::Cell { sheet, row, col } => {
8084                    if *row == 0 || *col == 0 {
8085                        return Err(ExcelError::new(ExcelErrorKind::Ref)
8086                            .with_message("target cell coordinates are one-based"));
8087                    }
8088                    let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
8089                        ExcelError::new(ExcelErrorKind::Ref)
8090                            .with_message(format!("target sheet not found: {sheet}"))
8091                    })?;
8092                    let cell = CellRef::new(sheet_id, Coord::from_excel(*row, *col, true, true));
8093                    resolve_region(
8094                        self,
8095                        Region::point(sheet_id, *row - 1, *col - 1),
8096                        Some(cell),
8097                        &mut roots,
8098                    )?;
8099                }
8100                crate::engine::EvaluationTarget::Range(range) => {
8101                    let sheet_id = self.graph.sheet_id(&range.sheet).ok_or_else(|| {
8102                        ExcelError::new(ExcelErrorKind::Ref)
8103                            .with_message(format!("target sheet not found: {}", range.sheet))
8104                    })?;
8105                    resolve_region(
8106                        self,
8107                        Region::rect(
8108                            sheet_id,
8109                            range.start_row - 1,
8110                            range.end_row - 1,
8111                            range.start_col - 1,
8112                            range.end_col - 1,
8113                        ),
8114                        None,
8115                        &mut roots,
8116                    )?;
8117                }
8118                crate::engine::EvaluationTarget::Name { name, scope_sheet } => {
8119                    let scope = self.name_query_scope(scope_sheet.as_deref())?;
8120                    if let Some(entry) = self.graph.resolve_name_entry_in_scope(name, scope) {
8121                        roots
8122                            .push(TargetProducer::Symbol(entry.vertex))
8123                            .map_err(|_| {
8124                                target_root_allocation_error(roots.len() + 1, request_id)
8125                            })?;
8126                    }
8127                }
8128                crate::engine::EvaluationTarget::Table { name, .. } => {
8129                    if let Some(entry) = self.graph.resolve_table_entry(name) {
8130                        roots
8131                            .push(TargetProducer::Symbol(entry.vertex))
8132                            .map_err(|_| {
8133                                target_root_allocation_error(roots.len() + 1, request_id)
8134                            })?;
8135                    }
8136                }
8137            }
8138        }
8139        Ok(roots.into_vec())
8140    }
8141
8142    /// Transactionally prepare the complete ordinary staged demand closure for typed targets.
8143    ///
8144    /// This method prepares graph topology only. It does not evaluate target values.
8145    pub fn prepare_graph_for_targets(
8146        &mut self,
8147        targets: &[crate::engine::EvaluationTarget],
8148        options: crate::engine::TargetEvalOptions<'_>,
8149    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
8150        let previous_budgets = self.evaluation_resource_budgets.clone();
8151        let diagnostics_len = self.formula_parse_diagnostics.len();
8152        let previous_report = self.last_formula_ingest_report.clone();
8153        if let Some(budgets) = options.budgets {
8154            self.evaluation_resource_budgets = budgets.clone();
8155        }
8156        let previous_graph_budget_override = self
8157            .graph
8158            .set_admission_budget_override(Some(self.evaluation_resource_budgets.clone()));
8159        // Hoist the call's cancellation onto the engine for its duration, so the
8160        // preparation checkpoints observe it. This is a standalone entry point, so
8161        // the previous value is restored rather than cleared.
8162        let previous_cancel = self.active_cancel_flag.take();
8163        self.active_cancel_flag = options.cancel.clone();
8164        let result = self.observe_evaluation_resource_request(
8165            EvaluationRequestKind::TargetPreparation,
8166            |engine| engine.prepare_graph_for_targets_unobserved(targets, &options),
8167        );
8168        self.active_cancel_flag = previous_cancel;
8169        self.graph
8170            .set_admission_budget_override(previous_graph_budget_override);
8171        self.evaluation_resource_budgets = previous_budgets;
8172        if result.is_err() {
8173            self.formula_parse_diagnostics.truncate(diagnostics_len);
8174            self.last_formula_ingest_report = previous_report;
8175        }
8176        result
8177    }
8178
8179    fn prepare_graph_for_targets_unobserved(
8180        &mut self,
8181        targets: &[crate::engine::EvaluationTarget],
8182        options: &crate::engine::TargetEvalOptions<'_>,
8183    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
8184        let scratch_checkpoint = self
8185            .active_resource_ledger
8186            .as_ref()
8187            .map_or(0, crate::engine::ResourceLedger::scratch_checkpoint);
8188        let result = self.prepare_graph_for_targets_transaction(targets, options);
8189        let release = self
8190            .active_resource_ledger
8191            .as_mut()
8192            .map_or(Ok(()), |ledger| {
8193                ledger.release_scratch_to(scratch_checkpoint)
8194            });
8195        match (result, release) {
8196            (result, Ok(())) => result,
8197            (Ok(_), Err(error)) | (Err(_), Err(error)) => Err(error.into_excel_error()),
8198        }
8199    }
8200
8201    fn prepare_graph_for_targets_transaction(
8202        &mut self,
8203        targets: &[crate::engine::EvaluationTarget],
8204        options: &crate::engine::TargetEvalOptions<'_>,
8205    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
8206        use crate::engine::{
8207            OpaqueReason, PreparationOutcome, PrepareScope, PreparedTargetGraphReport,
8208            TableSelection,
8209        };
8210
8211        self.target_preparation_checkpoint(options.deadline, 0)?;
8212        self.observe_function_semantic_epoch()?;
8213        let assumptions = self.preparation_revisions();
8214        let ledger_at_start = self
8215            .active_resource_ledger
8216            .as_ref()
8217            .map(|ledger| ledger.snapshot());
8218        let diagnostics_len = self.formula_parse_diagnostics.len();
8219        let report_len = self.last_formula_ingest_report.clone();
8220        let request_id = options.request_id.or_else(|| {
8221            self.active_evaluation_resource_request
8222                .as_ref()
8223                .map(|stats| stats.request_id)
8224        });
8225
8226        let mut scope = PrepareScope::Exact;
8227        let mut reasons = Vec::new();
8228        let mut regions = VecDeque::new();
8229        let mut deferred_shared_regions = VecDeque::new();
8230        let mut normalized = Vec::with_capacity(targets.len());
8231        let mut symbol_vertices = VecDeque::new();
8232        for target in targets {
8233            self.target_preparation_checkpoint(options.deadline, 1)?;
8234            match target {
8235                crate::engine::EvaluationTarget::Cell { sheet, row, col } => {
8236                    if *row == 0 || *col == 0 {
8237                        return Err(ExcelError::new(ExcelErrorKind::Ref)
8238                            .with_message("target cell coordinates are one-based"));
8239                    }
8240                    let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
8241                        ExcelError::new(ExcelErrorKind::Ref)
8242                            .with_message(format!("target sheet not found: {sheet}"))
8243                    })?;
8244                    regions.push_back(PreparationRegion {
8245                        sheet: sheet.clone(),
8246                        sheet_id,
8247                        start_row: *row,
8248                        start_col: *col,
8249                        end_row: *row,
8250                        end_col: *col,
8251                    });
8252                    normalized.push(target.clone());
8253                }
8254                crate::engine::EvaluationTarget::Range(range) => {
8255                    if range.start_row == 0
8256                        || range.start_col == 0
8257                        || range.end_row < range.start_row
8258                        || range.end_col < range.start_col
8259                    {
8260                        return Err(ExcelError::new(ExcelErrorKind::Ref)
8261                            .with_message("invalid target range"));
8262                    }
8263                    let sheet_id = self.graph.sheet_id(&range.sheet).ok_or_else(|| {
8264                        ExcelError::new(ExcelErrorKind::Ref)
8265                            .with_message(format!("target sheet not found: {}", range.sheet))
8266                    })?;
8267                    regions.push_back(PreparationRegion {
8268                        sheet: range.sheet.clone(),
8269                        sheet_id,
8270                        start_row: range.start_row,
8271                        start_col: range.start_col,
8272                        end_row: range.end_row,
8273                        end_col: range.end_col,
8274                    });
8275                    normalized.push(target.clone());
8276                }
8277                crate::engine::EvaluationTarget::Name { name, scope_sheet } => {
8278                    let name_scope = self.name_query_scope(scope_sheet.as_deref())?;
8279                    if let Some(entry) = self.graph.resolve_name_entry_in_scope(name, name_scope) {
8280                        symbol_vertices.push_back(entry.vertex);
8281                    } else {
8282                        Self::widen_target_preparation(
8283                            options.opaque_policy,
8284                            &mut scope,
8285                            &mut reasons,
8286                            OpaqueReason::UnresolvedName,
8287                        )?;
8288                    }
8289                    normalized.push(target.clone());
8290                }
8291                crate::engine::EvaluationTarget::Table { name, selection } => {
8292                    if let Some(entry) = self.graph.resolve_table_entry(name) {
8293                        let region = self.table_selection_region(entry, selection)?;
8294                        symbol_vertices.push_back(entry.vertex);
8295                        regions.push_back(region);
8296                    } else {
8297                        Self::widen_target_preparation(
8298                            options.opaque_policy,
8299                            &mut scope,
8300                            &mut reasons,
8301                            OpaqueReason::UnresolvedTable,
8302                        )?;
8303                    }
8304                    normalized.push(target.clone());
8305                }
8306            }
8307        }
8308
8309        let mut visited_regions = FxHashSet::default();
8310        let mut visited_vertices = FxHashSet::default();
8311        let mut selected = FxHashSet::default();
8312        let mut prepared = Vec::new();
8313        let mut pending_diagnostics = Vec::new();
8314        let mut planning_requests = BTreeSet::new();
8315        let mut selected_cells = Vec::new();
8316        let mut workbook_seeded = false;
8317        let mut sheet_scope_seeded = BTreeSet::new();
8318        let mut indexed_query_sheets = FxHashSet::default();
8319        let mut discovery_scratch_reserved = 0u64;
8320        let mut package_encountered = false;
8321        let mut selected_package_sheets = FxHashSet::default();
8322        let mut selected_package_points: BTreeMap<String, BTreeSet<(u32, u32)>> = BTreeMap::new();
8323        let mut prepared_packages: Vec<PreparedTargetSourcePackage> = Vec::new();
8324        let authoritative_with_ordinary = false;
8325        let has_unknown_package_sheet = self
8326            .staged_formula_index
8327            .package_sheets()
8328            .any(|sheet| self.graph.sheet_id(sheet).is_none());
8329
8330        loop {
8331            if let PrepareScope::Sheets(sheets) = &scope {
8332                for sheet in sheets.clone() {
8333                    if !sheet_scope_seeded.insert(sheet.clone()) {
8334                        continue;
8335                    }
8336                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
8337                        package_encountered = true;
8338                        Self::widen_target_preparation(
8339                            options.opaque_policy,
8340                            &mut scope,
8341                            &mut reasons,
8342                            OpaqueReason::UnsupportedSourceSemantics,
8343                        )?;
8344                        continue;
8345                    };
8346                    for lease in self.staged_formula_index.leases_for_sheet(&sheet) {
8347                        self.target_preparation_checkpoint(options.deadline, 1)?;
8348                        regions.push_back(PreparationRegion {
8349                            sheet: sheet.clone(),
8350                            sheet_id,
8351                            start_row: lease.row,
8352                            start_col: lease.col,
8353                            end_row: lease.row,
8354                            end_col: lease.col,
8355                        });
8356                    }
8357                    if self
8358                        .staged_formula_index
8359                        .package_lease_for_sheet(&sheet)
8360                        .is_some()
8361                    {
8362                        regions.push_back(PreparationRegion {
8363                            sheet: sheet.clone(),
8364                            sheet_id,
8365                            start_row: 1,
8366                            start_col: 1,
8367                            end_row: self.workbook_load_limits.max_sheet_rows,
8368                            end_col: self.workbook_load_limits.max_sheet_cols,
8369                        });
8370                    }
8371                }
8372            }
8373            if matches!(scope, PrepareScope::Workbook) && !workbook_seeded {
8374                workbook_seeded = true;
8375                for (sheet, lease) in self.staged_formula_index.all_leases() {
8376                    self.target_preparation_checkpoint(options.deadline, 1)?;
8377                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
8378                        package_encountered = true;
8379                        Self::widen_target_preparation(
8380                            options.opaque_policy,
8381                            &mut scope,
8382                            &mut reasons,
8383                            OpaqueReason::UnsupportedSourceSemantics,
8384                        )?;
8385                        continue;
8386                    };
8387                    regions.push_back(PreparationRegion {
8388                        sheet,
8389                        sheet_id,
8390                        start_row: lease.row,
8391                        start_col: lease.col,
8392                        end_row: lease.row,
8393                        end_col: lease.col,
8394                    });
8395                }
8396                let package_sheets = self
8397                    .staged_formula_index
8398                    .package_sheets()
8399                    .map(str::to_string)
8400                    .collect::<Vec<_>>();
8401                for sheet in package_sheets {
8402                    let Some(sheet_id) = self.graph.sheet_id(&sheet) else {
8403                        package_encountered = true;
8404                        Self::widen_target_preparation(
8405                            options.opaque_policy,
8406                            &mut scope,
8407                            &mut reasons,
8408                            OpaqueReason::UnsupportedSourceSemantics,
8409                        )?;
8410                        continue;
8411                    };
8412                    regions.push_back(PreparationRegion {
8413                        sheet,
8414                        sheet_id,
8415                        start_row: 1,
8416                        start_col: 1,
8417                        end_row: self.workbook_load_limits.max_sheet_rows,
8418                        end_col: self.workbook_load_limits.max_sheet_cols,
8419                    });
8420                }
8421            }
8422
8423            // Finish known ordinary/name dependency discovery before expanding
8424            // partial shared demands. A queued SUM may complete those families.
8425            let allow_partial_shared = regions.is_empty() && symbol_vertices.is_empty();
8426            let next_region = if allow_partial_shared {
8427                deferred_shared_regions.pop_front()
8428            } else {
8429                regions.pop_front()
8430            };
8431            let Some(region) = next_region else {
8432                if let Some(vertex) = symbol_vertices.pop_front() {
8433                    self.target_preparation_checkpoint(options.deadline, 1)?;
8434                    if !visited_vertices.insert(vertex) || !self.graph.vertex_exists(vertex) {
8435                        continue;
8436                    }
8437                    let vertex_is_dynamic = self.graph.is_dynamic(vertex);
8438                    if let Some(ast) = self.graph.get_formula(vertex) {
8439                        let snapshot =
8440                            self.target_planning_snapshot(&ast, &mut planning_requests)?;
8441                        if let Some(reason) =
8442                            Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
8443                        {
8444                            return Err(Self::preparation_stale(
8445                                reason,
8446                                "target planning snapshot became stale during discovery",
8447                            ));
8448                        }
8449                        let opaque = self.opaque_reason_in_ast(&ast, &snapshot);
8450                        if let Some(reason) =
8451                            opaque.or(vertex_is_dynamic.then_some(OpaqueReason::DynamicReference))
8452                        {
8453                            if reason == OpaqueReason::DynamicReference
8454                                && Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot)
8455                            {
8456                                let sheet = self.graph.get_vertex_sheet_id(vertex);
8457                                let sheet = self.graph.sheet_name(sheet).to_string();
8458                                Self::widen_target_preparation_to_sheet(
8459                                    options.opaque_policy,
8460                                    &mut scope,
8461                                    &mut reasons,
8462                                    reason,
8463                                    &sheet,
8464                                )?;
8465                            } else {
8466                                Self::widen_target_preparation(
8467                                    options.opaque_policy,
8468                                    &mut scope,
8469                                    &mut reasons,
8470                                    reason,
8471                                )?;
8472                            }
8473                        }
8474                    } else if vertex_is_dynamic {
8475                        Self::widen_target_preparation(
8476                            options.opaque_policy,
8477                            &mut scope,
8478                            &mut reasons,
8479                            OpaqueReason::DynamicReference,
8480                        )?;
8481                    }
8482                    if let Some(anchor) = self
8483                        .graph
8484                        .get_cell_ref(vertex)
8485                        .and_then(|cell| self.graph.spill_registry_anchor_for_cell(cell))
8486                    {
8487                        symbol_vertices.push_back(anchor);
8488                    }
8489                    if let Some(cell) = self.graph.get_cell_ref(vertex) {
8490                        let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
8491                        regions.push_back(PreparationRegion {
8492                            sheet,
8493                            sheet_id: cell.sheet_id,
8494                            start_row: cell.coord.row() + 1,
8495                            start_col: cell.coord.col() + 1,
8496                            end_row: cell.coord.row() + 1,
8497                            end_col: cell.coord.col() + 1,
8498                        });
8499                    }
8500                    // The formula's direct precedents, from the authority:
8501                    // cells and ranges become regions, symbol rows (names,
8502                    // tables, sources) their vertices.
8503                    match self.graph.authority_vertex_precedents(vertex) {
8504                        Some(precedents) => {
8505                            for (sheet_id, rect) in precedents {
8506                                self.target_preparation_checkpoint(options.deadline, 1)?;
8507                                if sheet_id == crate::engine::authority::geom::SYMBOL_SHEET {
8508                                    for slot in rect.r0..=rect.r1 {
8509                                        if let Some(symbol) =
8510                                            self.graph.authority_host().symbols().vertex(slot)
8511                                        {
8512                                            symbol_vertices.push_back(symbol);
8513                                        }
8514                                    }
8515                                    continue;
8516                                }
8517                                let sheet = self.graph.sheet_name(sheet_id).to_string();
8518                                regions.push_back(PreparationRegion {
8519                                    sheet,
8520                                    sheet_id,
8521                                    start_row: rect.r0 + 1,
8522                                    start_col: rect.c0 + 1,
8523                                    end_row: (rect.r1 + 1)
8524                                        .min(self.workbook_load_limits.max_sheet_rows),
8525                                    end_col: (rect.c1 + 1)
8526                                        .min(self.workbook_load_limits.max_sheet_cols),
8527                                });
8528                            }
8529                        }
8530                        // The authority cannot answer (failed host): widen.
8531                        None => {
8532                            if Self::widen_target_preparation(
8533                                options.opaque_policy,
8534                                &mut scope,
8535                                &mut reasons,
8536                                OpaqueReason::UnresolvedCrossSheetBinding,
8537                            )? {
8538                                continue;
8539                            }
8540                        }
8541                    }
8542                    if let Some(name) = self.graph.named_range_by_vertex(vertex).cloned() {
8543                        match &name.definition {
8544                            NamedDefinition::Cell(cell) => regions.push_back(PreparationRegion {
8545                                sheet: self.graph.sheet_name(cell.sheet_id).to_string(),
8546                                sheet_id: cell.sheet_id,
8547                                start_row: cell.coord.row() + 1,
8548                                start_col: cell.coord.col() + 1,
8549                                end_row: cell.coord.row() + 1,
8550                                end_col: cell.coord.col() + 1,
8551                            }),
8552                            NamedDefinition::Range(range) => regions.push_back(PreparationRegion {
8553                                sheet: self.graph.sheet_name(range.start.sheet_id).to_string(),
8554                                sheet_id: range.start.sheet_id,
8555                                start_row: range.start.coord.row() + 1,
8556                                start_col: range.start.coord.col() + 1,
8557                                end_row: range.end.coord.row() + 1,
8558                                end_col: range.end.coord.col() + 1,
8559                            }),
8560                            NamedDefinition::Formula {
8561                                ast,
8562                                dependencies,
8563                                range_deps,
8564                            } => {
8565                                let snapshot =
8566                                    self.target_planning_snapshot(ast, &mut planning_requests)?;
8567                                if let Some(reason) = Self::target_planning_snapshot_stale_reason(
8568                                    &snapshot,
8569                                    &assumptions,
8570                                ) {
8571                                    return Err(Self::preparation_stale(
8572                                        reason,
8573                                        "target planning snapshot became stale during discovery",
8574                                    ));
8575                                }
8576                                if let Some(reason) = self.opaque_reason_in_ast(ast, &snapshot) {
8577                                    Self::widen_target_preparation(
8578                                        options.opaque_policy,
8579                                        &mut scope,
8580                                        &mut reasons,
8581                                        reason,
8582                                    )?;
8583                                }
8584                                for dependency in dependencies {
8585                                    self.target_preparation_checkpoint(options.deadline, 1)?;
8586                                    symbol_vertices.push_back(*dependency);
8587                                }
8588                                for range in range_deps {
8589                                    self.target_preparation_checkpoint(options.deadline, 1)?;
8590                                    // `Current` is the sheet this name's formula
8591                                    // was interpreted on, which is the sheet its
8592                                    // vertex is placed on -- the same derivation
8593                                    // the formula-vertex arm above uses. It is
8594                                    // never the workbook default sheet, and an
8595                                    // unresolvable `Name` widens instead of
8596                                    // silently landing on some other sheet.
8597                                    let context_sheet = self.graph.get_vertex_sheet_id(vertex);
8598                                    let Ok(sheet_id) =
8599                                        self.resolve_sheet_locator(&range.sheet, context_sheet)
8600                                    else {
8601                                        if Self::widen_target_preparation(
8602                                            options.opaque_policy,
8603                                            &mut scope,
8604                                            &mut reasons,
8605                                            OpaqueReason::UnresolvedCrossSheetBinding,
8606                                        )? {
8607                                            break;
8608                                        }
8609                                        continue;
8610                                    };
8611                                    regions.push_back(PreparationRegion {
8612                                        sheet: self.graph.sheet_name(sheet_id).to_string(),
8613                                        sheet_id,
8614                                        start_row: range
8615                                            .start_row
8616                                            .map_or(1, |bound| bound.index + 1),
8617                                        start_col: range
8618                                            .start_col
8619                                            .map_or(1, |bound| bound.index + 1),
8620                                        end_row: range.end_row.map_or(
8621                                            self.workbook_load_limits.max_sheet_rows,
8622                                            |bound| bound.index + 1,
8623                                        ),
8624                                        end_col: range.end_col.map_or(
8625                                            self.workbook_load_limits.max_sheet_cols,
8626                                            |bound| bound.index + 1,
8627                                        ),
8628                                    });
8629                                }
8630                            }
8631                            NamedDefinition::Literal(_) => {}
8632                        }
8633                    }
8634                    if let Some(table) = self.graph.table_by_vertex(vertex) {
8635                        regions
8636                            .push_back(self.table_selection_region(table, &TableSelection::Whole)?);
8637                    }
8638                    continue;
8639                }
8640                break;
8641            };
8642
8643            self.target_preparation_checkpoint(options.deadline, 1)?;
8644            if !visited_regions.insert(region.clone()) && !allow_partial_shared {
8645                continue;
8646            }
8647            if let PrepareScope::Sheets(sheets) = &mut scope
8648                && !sheets.iter().any(|sheet| sheet == &region.sheet)
8649            {
8650                sheets.push(region.sheet.clone());
8651                sheets.sort();
8652            }
8653            let package_match = self.staged_formula_index.package_for_region(
8654                &region.sheet,
8655                region.start_row,
8656                region.start_col,
8657                region.end_row,
8658                region.end_col,
8659            );
8660            let package_lease = match package_match {
8661                Some(Ok(lease)) => Some(lease),
8662                Some(Err(()))
8663                    if region.start_row == 1
8664                        && region.start_col == 1
8665                        && region.end_row == self.workbook_load_limits.max_sheet_rows
8666                        && region.end_col == self.workbook_load_limits.max_sheet_cols =>
8667                {
8668                    self.staged_formula_index
8669                        .package_lease_for_sheet(&region.sheet)
8670                }
8671                Some(Err(())) => {
8672                    Self::widen_target_preparation(
8673                        options.opaque_policy,
8674                        &mut scope,
8675                        &mut reasons,
8676                        OpaqueReason::DeferredSourcePackage,
8677                    )?;
8678                    None
8679                }
8680                None => None,
8681            };
8682            let compatibility_before_package_replay = package_lease.is_some()
8683                && (authoritative_with_ordinary || has_unknown_package_sheet);
8684            let package_lease = if compatibility_before_package_replay {
8685                package_encountered = true;
8686                Self::widen_target_preparation(
8687                    options.opaque_policy,
8688                    &mut scope,
8689                    &mut reasons,
8690                    OpaqueReason::UnsupportedSourceSemantics,
8691                )?;
8692                None
8693            } else {
8694                package_lease
8695            };
8696            if let Some(package_lease) = package_lease
8697                && !selected_package_sheets.contains(&region.sheet)
8698            {
8699                self.target_preparation_checkpoint(options.deadline, 1)?;
8700                let mut points = self.staged_formula_index.package_points_in_region(
8701                    &region.sheet,
8702                    region.start_row,
8703                    region.start_col,
8704                    region.end_row,
8705                    region.end_col,
8706                );
8707                if let Some(selected) = selected_package_points.get(&region.sheet) {
8708                    points.retain(|point| !selected.contains(point));
8709                }
8710
8711                if let Some(selected) = selected_package_points.get(&region.sheet) {
8712                    points.retain(|point| !selected.contains(point));
8713                }
8714                let permit_partial = allow_partial_shared
8715                    || (points.len() == 1
8716                        && regions.is_empty()
8717                        && symbol_vertices.is_empty()
8718                        && deferred_shared_regions.is_empty());
8719                let partial = self.prepare_target_exact_source_selection(
8720                    &region.sheet,
8721                    package_lease,
8722                    points,
8723                    selected_package_points
8724                        .get(&region.sheet)
8725                        .unwrap_or(&BTreeSet::new()),
8726                    permit_partial,
8727                    options.deadline,
8728                    &mut discovery_scratch_reserved,
8729                )?;
8730                let mut package = if let Some(mut package) = partial {
8731                    if package.deferred_shared {
8732                        deferred_shared_regions.push_back(region.clone());
8733                    }
8734                    // A later range can complete a family touched earlier in this
8735                    // request. Retire only that family's earlier legacy proposals;
8736                    // nothing has been published and its dependencies remain demanded.
8737                    if !package.direct_domains.is_empty() {
8738                        for prior in prepared_packages
8739                            .iter_mut()
8740                            .filter(|prior| prior.sheet == region.sheet)
8741                        {
8742                            prior
8743                                .replay_records
8744                                .retain(|record| !package.direct_contains(record.row, record.col));
8745                            prior
8746                                .legacy
8747                                .retain(|(row, col, _, _)| !package.direct_contains(*row, *col));
8748                            if let Some(points) = prior.selected_points.as_mut() {
8749                                points.retain(|&(row, col)| !package.direct_contains(row, col));
8750                            }
8751                        }
8752                    }
8753                    if selected_package_points.contains_key(&region.sheet) {
8754                        package.source_report = Default::default();
8755                    }
8756                    let points = package.selected_points.as_ref().unwrap();
8757                    if !points.is_empty() {
8758                        selected_package_points
8759                            .entry(region.sheet.clone())
8760                            .or_default()
8761                            .extend(points.iter().copied());
8762                    }
8763                    package
8764                } else {
8765                    selected_package_sheets.insert(region.sheet.clone());
8766                    self.prepare_target_source_package(
8767                        &region.sheet,
8768                        package_lease,
8769                        options.deadline,
8770                    )?
8771                };
8772                if package
8773                    .selected_points
8774                    .as_ref()
8775                    .is_none_or(|points| !points.is_empty())
8776                {
8777                    let mut final_fallback = BTreeMap::new();
8778                    for record in package.fallback_records() {
8779                        final_fallback.insert((record.row, record.col), record.clone());
8780                    }
8781                    let batch = self.formula_batch_from_exact_replay(
8782                        &region.sheet,
8783                        final_fallback.into_values(),
8784                    )?;
8785                    for record in batch.formulas {
8786                        self.target_preparation_checkpoint(options.deadline, 1)?;
8787                        let ast = self
8788                            .graph
8789                            .data_store()
8790                            .retrieve_ast(record.ast_id, self.graph.sheet_reg())
8791                            .ok_or_else(|| {
8792                                ExcelError::new(ExcelErrorKind::Value)
8793                                    .with_message("target fallback AST is unavailable")
8794                            })?;
8795                        let snapshot =
8796                            self.target_planning_snapshot(&ast, &mut planning_requests)?;
8797                        if let Some(reason) =
8798                            Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
8799                        {
8800                            return Err(Self::preparation_stale(
8801                                reason,
8802                                "target fallback planning snapshot became stale during discovery",
8803                            ));
8804                        }
8805                        let proven_sheet_local_dynamic =
8806                            Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot);
8807                        if let Some(reason) = self.opaque_reason_in_ast(&ast, &snapshot) {
8808                            if reason == OpaqueReason::DynamicReference
8809                                && proven_sheet_local_dynamic
8810                            {
8811                                Self::widen_target_preparation_to_sheet(
8812                                    options.opaque_policy,
8813                                    &mut scope,
8814                                    &mut reasons,
8815                                    reason,
8816                                    &region.sheet,
8817                                )?;
8818                            } else {
8819                                Self::widen_target_preparation(
8820                                    options.opaque_policy,
8821                                    &mut scope,
8822                                    &mut reasons,
8823                                    reason,
8824                                )?;
8825                            }
8826                        }
8827                        let placement = CellRef::new(
8828                            package.sheet_id,
8829                            Coord::from_excel(record.row, record.col, true, true),
8830                        );
8831                        let ingested = self
8832                            .graph
8833                            .ingest_pipeline(&snapshot)
8834                            .enable_function_semantics()
8835                            .ingest_formula(
8836                                FormulaAstInput::RawArena(record.ast_id),
8837                                placement,
8838                                record.formula_text,
8839                            )?;
8840                        if ingested.dep_plan.dynamic {
8841                            if proven_sheet_local_dynamic {
8842                                Self::widen_target_preparation_to_sheet(
8843                                    options.opaque_policy,
8844                                    &mut scope,
8845                                    &mut reasons,
8846                                    OpaqueReason::DynamicReference,
8847                                    &region.sheet,
8848                                )?;
8849                            } else {
8850                                Self::widen_target_preparation(
8851                                    options.opaque_policy,
8852                                    &mut scope,
8853                                    &mut reasons,
8854                                    OpaqueReason::DynamicReference,
8855                                )?;
8856                            }
8857                        }
8858                        for dep in &ingested.dep_plan.direct_cell_deps {
8859                            self.target_preparation_checkpoint(options.deadline, 1)?;
8860                            regions.push_back(PreparationRegion {
8861                                sheet: self.graph.sheet_name(dep.sheet_id).to_string(),
8862                                sheet_id: dep.sheet_id,
8863                                start_row: dep.coord.row().saturating_add(1),
8864                                start_col: dep.coord.col().saturating_add(1),
8865                                end_row: dep.coord.row().saturating_add(1),
8866                                end_col: dep.coord.col().saturating_add(1),
8867                            });
8868                        }
8869                        for range in &ingested.dep_plan.range_deps {
8870                            self.target_preparation_checkpoint(options.deadline, 1)?;
8871                            // `Current` is the sheet the staged package's formula
8872                            // lives on.
8873                            let Ok(dependency_sheet) =
8874                                self.resolve_sheet_locator(&range.sheet, package.sheet_id)
8875                            else {
8876                                Self::widen_target_preparation(
8877                                    options.opaque_policy,
8878                                    &mut scope,
8879                                    &mut reasons,
8880                                    OpaqueReason::UnresolvedCrossSheetBinding,
8881                                )?;
8882                                continue;
8883                            };
8884                            regions.push_back(PreparationRegion {
8885                                sheet: self.graph.sheet_name(dependency_sheet).to_string(),
8886                                sheet_id: dependency_sheet,
8887                                start_row: range.start_row.map_or(1, |bound| bound.index + 1),
8888                                start_col: range.start_col.map_or(1, |bound| bound.index + 1),
8889                                end_row: range
8890                                    .end_row
8891                                    .map_or(self.workbook_load_limits.max_sheet_rows, |bound| {
8892                                        bound.index + 1
8893                                    }),
8894                                end_col: range
8895                                    .end_col
8896                                    .map_or(self.workbook_load_limits.max_sheet_cols, |bound| {
8897                                        bound.index + 1
8898                                    }),
8899                            });
8900                        }
8901                        for name in ingested
8902                            .dep_plan
8903                            .resolved_named_refs
8904                            .iter()
8905                            .chain(&ingested.dep_plan.named_refs)
8906                        {
8907                            self.target_preparation_checkpoint(options.deadline, 1)?;
8908                            if let Some(entry) =
8909                                self.graph.resolve_name_entry(name, package.sheet_id)
8910                            {
8911                                symbol_vertices.push_back(entry.vertex);
8912                            } else if self.graph.resolve_source_scalar_entry(name).is_none()
8913                                && self.graph.resolve_source_table_entry(name).is_none()
8914                            {
8915                                Self::widen_target_preparation(
8916                                    options.opaque_policy,
8917                                    &mut scope,
8918                                    &mut reasons,
8919                                    OpaqueReason::UnresolvedName,
8920                                )?;
8921                            }
8922                        }
8923                        for table in &ingested.dep_plan.table_refs {
8924                            self.target_preparation_checkpoint(options.deadline, 1)?;
8925                            if let Some(entry) = self.graph.resolve_table_entry(table) {
8926                                symbol_vertices.push_back(entry.vertex);
8927                            } else if self.graph.resolve_source_table_entry(table).is_none() {
8928                                Self::widen_target_preparation(
8929                                    options.opaque_policy,
8930                                    &mut scope,
8931                                    &mut reasons,
8932                                    OpaqueReason::UnresolvedTable,
8933                                )?;
8934                            }
8935                        }
8936                        package.legacy.push((
8937                            record.row,
8938                            record.col,
8939                            ingested.ast_id,
8940                            ingested.dep_plan,
8941                        ));
8942                    }
8943                    prepared_packages.push(package);
8944                }
8945            }
8946            let leases = self.staged_formula_index.leases_in_region(
8947                &region.sheet,
8948                region.start_row,
8949                region.start_col,
8950                region.end_row,
8951                region.end_col,
8952            );
8953            for lease in leases {
8954                self.target_preparation_checkpoint(options.deadline, 1)?;
8955                let sheet_id = self.graph.sheet_id(&region.sheet).ok_or_else(|| {
8956                    ExcelError::new(ExcelErrorKind::Ref)
8957                        .with_message(format!("staged formula sheet not found: {}", region.sheet))
8958                })?;
8959                let key = (region.sheet.clone(), lease.row, lease.col, lease.generation);
8960                if !selected.insert(key) {
8961                    continue;
8962                }
8963                let text = self
8964                    .staged_formulas
8965                    .get(&region.sheet)
8966                    .and_then(|sheet| sheet.get_ordinary(lease.row, lease.col))
8967                    .ok_or_else(|| {
8968                        ExcelError::new(ExcelErrorKind::Value)
8969                            .with_message("staged formula index is stale")
8970                    })?
8971                    .to_string();
8972                let formula = if text.starts_with('=') {
8973                    text.clone()
8974                } else {
8975                    format!("={text}")
8976                };
8977                self.target_preparation_checkpoint(options.deadline, 1)?;
8978                let ast = match formualizer_parse::parser::parse(&formula) {
8979                    Ok(ast) => ast,
8980                    Err(error) => {
8981                        if self.config.formula_parse_policy == FormulaParsePolicy::Strict {
8982                            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
8983                                format!(
8984                                    "Formula parse error at {}!{}{}: {error}",
8985                                    region.sheet,
8986                                    col_letters_from_1based(lease.col)
8987                                        .unwrap_or_else(|_| "?".to_string()),
8988                                    lease.row
8989                                ),
8990                            ));
8991                        }
8992                        pending_diagnostics.push(FormulaParseDiagnostic {
8993                            sheet: region.sheet.clone(),
8994                            row: lease.row,
8995                            col: lease.col,
8996                            formula: formula.clone(),
8997                            message: error.to_string(),
8998                            policy: self.config.formula_parse_policy,
8999                        });
9000                        match self.config.formula_parse_policy {
9001                            FormulaParsePolicy::KeepCachedValue => {
9002                                selected_cells.push(
9003                                    formualizer_common::RangeAddress::new(
9004                                        region.sheet.clone(),
9005                                        lease.row,
9006                                        lease.col,
9007                                        lease.row,
9008                                        lease.col,
9009                                    )
9010                                    .expect("selected staged coordinates are valid"),
9011                                );
9012                                prepared.push(PreparedOrdinaryStagedFormula {
9013                                    sheet: region.sheet.clone(),
9014                                    sheet_id,
9015                                    lease,
9016                                    ast_id: None,
9017                                    plan: None,
9018                                });
9019                                continue;
9020                            }
9021                            FormulaParsePolicy::AsText => ASTNode::new(
9022                                ASTNodeType::Literal(LiteralValue::Text(formula.clone())),
9023                                None,
9024                            ),
9025                            FormulaParsePolicy::CoerceToError => ASTNode::new(
9026                                ASTNodeType::Literal(LiteralValue::Error(
9027                                    ExcelError::new(ExcelErrorKind::Error)
9028                                        .with_message(format!("Malformed formula: {error}")),
9029                                )),
9030                                None,
9031                            ),
9032                            FormulaParsePolicy::Strict => unreachable!(),
9033                        }
9034                    }
9035                };
9036                self.target_preparation_checkpoint(options.deadline, 1)?;
9037                let snapshot = self.target_planning_snapshot(&ast, &mut planning_requests)?;
9038                self.target_preparation_checkpoint(options.deadline, 1)?;
9039                if let Some(reason) =
9040                    Self::target_planning_snapshot_stale_reason(&snapshot, &assumptions)
9041                {
9042                    return Err(Self::preparation_stale(
9043                        reason,
9044                        "target planning snapshot became stale during discovery",
9045                    ));
9046                }
9047                if let Some(reason) = self.opaque_reason_in_ast(&ast, &snapshot) {
9048                    if reason == OpaqueReason::DynamicReference
9049                        && Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot)
9050                    {
9051                        Self::widen_target_preparation_to_sheet(
9052                            options.opaque_policy,
9053                            &mut scope,
9054                            &mut reasons,
9055                            reason,
9056                            &region.sheet,
9057                        )?;
9058                    } else {
9059                        Self::widen_target_preparation(
9060                            options.opaque_policy,
9061                            &mut scope,
9062                            &mut reasons,
9063                            reason,
9064                        )?;
9065                    }
9066                }
9067                let proven_sheet_local_dynamic =
9068                    Self::ast_has_proven_sheet_local_dynamic(&ast, &snapshot);
9069                let placement = CellRef::new(
9070                    sheet_id,
9071                    Coord::from_excel(lease.row, lease.col, true, true),
9072                );
9073                let ingested = self.graph.ingest_pipeline(&snapshot).ingest_formula(
9074                    FormulaAstInput::Tree(ast),
9075                    placement,
9076                    Some(Arc::from(formula)),
9077                )?;
9078                self.target_preparation_checkpoint(options.deadline, 1)?;
9079                if ingested.dep_plan.dynamic {
9080                    if proven_sheet_local_dynamic {
9081                        Self::widen_target_preparation_to_sheet(
9082                            options.opaque_policy,
9083                            &mut scope,
9084                            &mut reasons,
9085                            OpaqueReason::DynamicReference,
9086                            &region.sheet,
9087                        )?;
9088                    } else {
9089                        Self::widen_target_preparation(
9090                            options.opaque_policy,
9091                            &mut scope,
9092                            &mut reasons,
9093                            OpaqueReason::DynamicReference,
9094                        )?;
9095                    }
9096                }
9097                for dep in &ingested.dep_plan.direct_cell_deps {
9098                    self.target_preparation_checkpoint(options.deadline, 1)?;
9099                    regions.push_back(PreparationRegion {
9100                        sheet: self.graph.sheet_name(dep.sheet_id).to_string(),
9101                        sheet_id: dep.sheet_id,
9102                        start_row: dep.coord.row() + 1,
9103                        start_col: dep.coord.col() + 1,
9104                        end_row: dep.coord.row() + 1,
9105                        end_col: dep.coord.col() + 1,
9106                    });
9107                }
9108                for range in &ingested.dep_plan.range_deps {
9109                    self.target_preparation_checkpoint(options.deadline, 1)?;
9110                    // `Current` is the sheet the staged formula lives on.
9111                    let Ok(dependency_sheet) = self.resolve_sheet_locator(&range.sheet, sheet_id)
9112                    else {
9113                        Self::widen_target_preparation(
9114                            options.opaque_policy,
9115                            &mut scope,
9116                            &mut reasons,
9117                            OpaqueReason::UnresolvedCrossSheetBinding,
9118                        )?;
9119                        continue;
9120                    };
9121                    regions.push_back(PreparationRegion {
9122                        sheet: self.graph.sheet_name(dependency_sheet).to_string(),
9123                        sheet_id: dependency_sheet,
9124                        start_row: range.start_row.map_or(1, |bound| bound.index + 1),
9125                        start_col: range.start_col.map_or(1, |bound| bound.index + 1),
9126                        end_row: range
9127                            .end_row
9128                            .map_or(self.workbook_load_limits.max_sheet_rows, |bound| {
9129                                bound.index + 1
9130                            }),
9131                        end_col: range
9132                            .end_col
9133                            .map_or(self.workbook_load_limits.max_sheet_cols, |bound| {
9134                                bound.index + 1
9135                            }),
9136                    });
9137                }
9138                for name in ingested
9139                    .dep_plan
9140                    .resolved_named_refs
9141                    .iter()
9142                    .chain(&ingested.dep_plan.named_refs)
9143                {
9144                    self.target_preparation_checkpoint(options.deadline, 1)?;
9145                    if let Some(entry) = self.graph.resolve_name_entry(name, sheet_id) {
9146                        symbol_vertices.push_back(entry.vertex);
9147                    } else if self.graph.resolve_source_scalar_entry(name).is_none()
9148                        && self.graph.resolve_source_table_entry(name).is_none()
9149                    {
9150                        Self::widen_target_preparation(
9151                            options.opaque_policy,
9152                            &mut scope,
9153                            &mut reasons,
9154                            OpaqueReason::UnresolvedName,
9155                        )?;
9156                    }
9157                }
9158                for table in &ingested.dep_plan.table_refs {
9159                    self.target_preparation_checkpoint(options.deadline, 1)?;
9160                    if let Some(entry) = self.graph.resolve_table_entry(table) {
9161                        symbol_vertices.push_back(entry.vertex);
9162                    } else if self.graph.resolve_source_table_entry(table).is_none() {
9163                        Self::widen_target_preparation(
9164                            options.opaque_policy,
9165                            &mut scope,
9166                            &mut reasons,
9167                            OpaqueReason::UnresolvedTable,
9168                        )?;
9169                    }
9170                }
9171                selected_cells.push(
9172                    formualizer_common::RangeAddress::new(
9173                        region.sheet.clone(),
9174                        lease.row,
9175                        lease.col,
9176                        lease.row,
9177                        lease.col,
9178                    )
9179                    .expect("selected staged coordinates are valid"),
9180                );
9181                prepared.push(PreparedOrdinaryStagedFormula {
9182                    sheet: region.sheet.clone(),
9183                    sheet_id,
9184                    lease,
9185                    ast_id: Some(ingested.ast_id),
9186                    plan: Some(ingested.dep_plan),
9187                });
9188            }
9189
9190            if indexed_query_sheets.insert(region.sheet_id) {
9191                self.graph.prepare_sheet_index_for_query(region.sheet_id);
9192                let bytes = (self.graph.sheet_index_vertex_count(region.sheet_id) as u64)
9193                    .saturating_mul(32);
9194                self.reserve_graph_source_scratch(bytes)?;
9195                discovery_scratch_reserved = discovery_scratch_reserved.saturating_add(bytes);
9196            }
9197            let spill_anchors = self.graph.spill_anchors_in_region(
9198                region.sheet_id,
9199                region.start_row - 1,
9200                region.start_col - 1,
9201                region.end_row - 1,
9202                region.end_col - 1,
9203            );
9204            for anchor in spill_anchors {
9205                self.target_preparation_checkpoint(options.deadline, 1)?;
9206                symbol_vertices.push_back(anchor);
9207            }
9208            let vertices = self.graph.vertices_in_region(
9209                region.sheet_id,
9210                region.start_row - 1,
9211                region.end_row - 1,
9212                region.start_col - 1,
9213                region.end_col - 1,
9214            );
9215            for vertex in vertices {
9216                self.target_preparation_checkpoint(options.deadline, 1)?;
9217                symbol_vertices.push_back(vertex);
9218            }
9219        }
9220
9221        #[cfg(test)]
9222        self.target_preparation_fault(
9223            crate::engine::target_preparation::TargetPreparationFault::AfterDiscovery,
9224        )?;
9225
9226        if package_encountered {
9227            Self::widen_target_preparation(
9228                options.opaque_policy,
9229                &mut scope,
9230                &mut reasons,
9231                OpaqueReason::UnsupportedSourceSemantics,
9232            )?;
9233            self.target_preparation_checkpoint(options.deadline, 0)?;
9234            let selected_count = self.staged_formula_count();
9235            let selected_packages = self
9236                .staged_formulas
9237                .values()
9238                .filter_map(|staged| staged.deferred_package.as_ref())
9239                .map(|package| package.families.len() + package.partitioned_families.len())
9240                .sum();
9241            self.formula_parse_diagnostics.truncate(diagnostics_len);
9242            self.last_formula_ingest_report = report_len;
9243            #[cfg(test)]
9244            if let Some(hook) = self.before_target_preparation_commit_hook.take() {
9245                hook();
9246            }
9247            self.target_preparation_checkpoint(options.deadline, 0)?;
9248            #[cfg(test)]
9249            self.target_preparation_fault(
9250                crate::engine::target_preparation::TargetPreparationFault::FinalRevisionValidation,
9251            )?;
9252            let current_revisions = self.preparation_revisions();
9253            if let Some(reason) = Self::preparation_revision_stale_reason(
9254                &assumptions,
9255                &current_revisions,
9256                &planning_requests,
9257                true,
9258            ) {
9259                return Err(Self::preparation_stale(
9260                    reason,
9261                    "target compatibility preparation plan is stale",
9262                ));
9263            }
9264            #[cfg(test)]
9265            self.target_preparation_fault(
9266                crate::engine::target_preparation::TargetPreparationFault::FinalGraphValidation,
9267            )?;
9268            let commit_work_before = self
9269                .active_resource_ledger
9270                .as_ref()
9271                .map_or(0, |ledger| ledger.snapshot().work_charged);
9272            let commit_started = crate::instant::FzInstant::now();
9273            self.build_graph_all_unobserved()?;
9274            let commit_window = commit_started.elapsed();
9275            let ledger_after = self
9276                .active_resource_ledger
9277                .as_ref()
9278                .map(|ledger| ledger.snapshot());
9279            let actual_commit_work = ledger_after
9280                .map_or(0, |snapshot| snapshot.work_charged)
9281                .saturating_sub(commit_work_before);
9282            let observed_scratch_bytes = ledger_after
9283                .map_or(0, |snapshot| snapshot.scratch_peak)
9284                .saturating_sub(ledger_at_start.map_or(0, |snapshot| snapshot.scratch_current));
9285            let revisions = assumptions.clone();
9286            let report = PreparedTargetGraphReport {
9287                request_id: request_id.unwrap_or_default(),
9288                requested_targets: targets.len(),
9289                normalized_regions: visited_regions.len(),
9290                normalized_target_list: normalized,
9291                selected_staged_cells: selected_count,
9292                selected_source_families: selected_packages,
9293                retained_staged_cells: self.staged_formula_count(),
9294                selected_cells,
9295                retained_cells: Vec::new(),
9296                widened_scope: PrepareScope::Workbook,
9297                widening_reasons: reasons,
9298                revisions,
9299                commit_window,
9300                estimated_scratch_bytes: discovery_scratch_reserved
9301                    .saturating_add((selected_count as u64).saturating_mul(256)),
9302                observed_scratch_bytes,
9303                estimated_commit_work: selected_count as u64,
9304                actual_commit_work,
9305                outcome: PreparationOutcome::CompatibilityPrepared,
9306            };
9307            self.observe_target_preparation_report(&report);
9308            return Ok(report);
9309        }
9310
9311        prepared.sort_by_key(|formula| formula.lease.insertion_order);
9312        for package in &prepared_packages {
9313            if let Some(points) = &package.selected_points {
9314                selected_cells.extend(points.iter().filter_map(|&(row, col)| {
9315                    formualizer_common::RangeAddress::new(&package.sheet, row, col, row, col).ok()
9316                }));
9317                continue;
9318            }
9319            selected_cells.extend(package.replay_records.iter().filter_map(|record| {
9320                formualizer_common::RangeAddress::new(
9321                    package.sheet.clone(),
9322                    record.row,
9323                    record.col,
9324                    record.row,
9325                    record.col,
9326                )
9327                .ok()
9328            }));
9329        }
9330        let (legacy_graph, planned_formula_count) =
9331            self.prepare_target_combined_legacy_graph(&prepared_packages, &prepared)?;
9332        let new_vertices = legacy_graph.new_vertex_count();
9333        let new_edges = legacy_graph.planned_edge_count().ok_or_else(|| {
9334            ExcelError::new(ExcelErrorKind::NImpl).with_message("target graph edge count overflow")
9335        })?;
9336        let removed_edges = legacy_graph.removed_edge_count().ok_or_else(|| {
9337            ExcelError::new(ExcelErrorKind::NImpl).with_message("target graph edge count overflow")
9338        })?;
9339        let current = self.graph.baseline_stats();
9340        let final_vertices = current
9341            .graph_vertex_count
9342            .checked_add(new_vertices)
9343            .ok_or_else(|| {
9344                crate::engine::ResourceLedgerError::Exhausted(
9345                    formualizer_common::ResourceExhaustionDetail {
9346                        reason: formualizer_common::ResourceExhaustionReason::ArithmeticOverflow,
9347                        limit: u64::MAX,
9348                        observed: u64::MAX,
9349                        request_id,
9350                    },
9351                )
9352                .into_excel_error()
9353            })?;
9354        let final_edges = current
9355            .graph_edge_count
9356            .checked_sub(removed_edges)
9357            .and_then(|count| count.checked_add(new_edges))
9358            .ok_or_else(|| {
9359                crate::engine::ResourceLedgerError::Exhausted(
9360                    formualizer_common::ResourceExhaustionDetail {
9361                        reason: formualizer_common::ResourceExhaustionReason::ArithmeticOverflow,
9362                        limit: u64::MAX,
9363                        observed: u64::MAX,
9364                        request_id,
9365                    },
9366                )
9367                .into_excel_error()
9368            })?;
9369        #[cfg(test)]
9370        self.target_preparation_fault(
9371            crate::engine::target_preparation::TargetPreparationFault::Admission,
9372        )?;
9373        let resource = |reason, limit: u64, observed: u64| {
9374            crate::engine::ResourceLedgerError::Exhausted(
9375                formualizer_common::ResourceExhaustionDetail {
9376                    reason,
9377                    limit,
9378                    observed,
9379                    request_id,
9380                },
9381            )
9382            .into_excel_error()
9383        };
9384        let admission = crate::engine::resource_ledger::GraphAdmission {
9385            final_vertices,
9386            final_edges,
9387            materialization_cells: planned_formula_count as u64,
9388            added_vertices: new_vertices,
9389            added_edges: new_edges,
9390        };
9391        let materialized_bytes = admission
9392            .materialized_graph_bytes()
9393            .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
9394        if let Err(error) = self.preflight_graph_admission(admission) {
9395            if let formualizer_common::ExcelErrorExtra::Resource { detail } = &error.extra {
9396                self.observe_target_admission_failure(detail.reason);
9397            }
9398            return Err(error);
9399        }
9400        let selected_package_records = prepared_packages
9401            .iter()
9402            .map(|package| package.replay_records.len() as u64)
9403            .sum::<u64>();
9404        let planned_working_bytes = (prepared.len() as u64)
9405            .saturating_add(selected_package_records)
9406            .saturating_mul(256)
9407            .saturating_add((visited_regions.len() as u64).saturating_mul(128))
9408            .saturating_add((visited_vertices.len() as u64).saturating_mul(32))
9409            .saturating_add((selected.len() as u64).saturating_mul(96))
9410            .saturating_add(materialized_bytes);
9411        let residual_scratch = selected_package_points
9412            .iter()
9413            .map(|(sheet, points)| {
9414                let source = self
9415                    .staged_formulas
9416                    .get(sheet)
9417                    .unwrap()
9418                    .deferred_package
9419                    .as_ref()
9420                    .unwrap();
9421                (source.families.len() as u64)
9422                    .saturating_mul(256)
9423                    .saturating_add(
9424                        source
9425                            .partitioned_families
9426                            .iter()
9427                            .map(|family| {
9428                                256u64
9429                                    .saturating_add(family.fragments.len() as u64 * 32)
9430                                    .saturating_add(family.legacy_members.len() as u64 * 32)
9431                            })
9432                            .sum::<u64>(),
9433                    )
9434                    // Direct complete domains need coordinate suppression, not
9435                    // a per-member residual split/proof or legacy AST reserve.
9436                    .saturating_add(points.len() as u64 * 32)
9437                    .saturating_add(
9438                        prepared_packages
9439                            .iter()
9440                            .filter(|package| &package.sheet == sheet)
9441                            .map(|package| package.replay_records.len() as u64 * 480)
9442                            .sum::<u64>(),
9443                    )
9444            })
9445            .sum::<u64>();
9446        let scratch_bytes = discovery_scratch_reserved
9447            .saturating_add(planned_working_bytes)
9448            .saturating_add(residual_scratch);
9449        let remaining_scratch = scratch_bytes.saturating_sub(discovery_scratch_reserved);
9450        if let Err(error) = self.reserve_graph_source_scratch(remaining_scratch) {
9451            self.observe_target_admission_failure(
9452                formualizer_common::ResourceExhaustionReason::ScratchMemory,
9453            );
9454            return Err(error);
9455        }
9456
9457        let mut residual_sources = BTreeMap::new();
9458        let mut residual_owners = BTreeMap::new();
9459        for (sheet, points) in &selected_package_points {
9460            let selected: BTreeMap<_, BTreeSet<_>> = prepared_packages
9461                .iter()
9462                .filter(|package| &package.sheet == sheet)
9463                .flat_map(|package| &package.replay_records)
9464                .filter_map(|record| {
9465                    record.partition_owner.or(record.family).map(|owner| {
9466                        (
9467                            owner,
9468                            crate::engine::SourceCoord {
9469                                row: record.row - 1,
9470                                col: record.col - 1,
9471                            },
9472                        )
9473                    })
9474                })
9475                .fold(BTreeMap::new(), |mut map, (owner, coord)| {
9476                    map.entry(owner).or_default().insert(coord);
9477                    map
9478                });
9479            let metadata_work = self
9480                .staged_formulas
9481                .get(sheet)
9482                .unwrap()
9483                .deferred_package
9484                .as_ref()
9485                .map_or(0, |source| {
9486                    source.families.len() as u64
9487                        + source
9488                            .partitioned_families
9489                            .iter()
9490                            .map(|family| {
9491                                1 + family.fragments.len() as u64 * family.fragments.len() as u64
9492                                    + family.legacy_members.len() as u64
9493                            })
9494                            .sum::<u64>()
9495                });
9496            let split_work = selected
9497                .values()
9498                .map(|points| points.len() as u64 * 128)
9499                .sum::<u64>();
9500            self.target_preparation_checkpoint(
9501                options.deadline,
9502                metadata_work
9503                    .saturating_add(split_work)
9504                    .saturating_add(points.len() as u64),
9505            )?;
9506            let source = self
9507                .staged_formulas
9508                .get(sheet)
9509                .unwrap()
9510                .deferred_package
9511                .as_ref()
9512                .unwrap();
9513            let complete: BTreeSet<_> = prepared_packages
9514                .iter()
9515                .filter(|package| &package.sheet == sheet)
9516                .flat_map(|package| package.complete_selections.iter().copied())
9517                .collect();
9518            let residual = source
9519                .residual_sources(&selected, &complete, &self.workbook_load_limits)
9520                .map_err(|reason| ExcelError::new(ExcelErrorKind::Value).with_message(reason))?;
9521            residual_owners.insert(
9522                sheet.clone(),
9523                residual
9524                    .1
9525                    .iter()
9526                    .map(|family| family.source_id)
9527                    .collect::<BTreeSet<_>>(),
9528            );
9529            residual_sources.insert(sheet.clone(), residual);
9530        }
9531
9532        // Reserve residual suppression before the revision-validated commit window.
9533        // Hash-set insertion and point-index removal below cannot allocate.
9534        for (sheet, points) in &selected_package_points {
9535            let package = self
9536                .staged_formulas
9537                .get_mut(sheet)
9538                .unwrap()
9539                .deferred_package
9540                .as_mut()
9541                .unwrap();
9542            let owner_count = prepared_packages
9543                .iter()
9544                .filter(|p| &p.sheet == sheet)
9545                .flat_map(|p| &p.replay_records)
9546                .filter_map(|record| record.partition_owner.or(record.family))
9547                .filter(|owner| residual_owners[sheet].contains(owner))
9548                .count();
9549            package
9550                .consumed_members
9551                .try_reserve(owner_count)
9552                .map_err(|_| {
9553                    resource(
9554                        formualizer_common::ResourceExhaustionReason::ScratchMemory,
9555                        0,
9556                        owner_count as u64 * 24,
9557                    )
9558                })?;
9559            package.suppressed.try_reserve(points.len()).map_err(|_| {
9560                resource(
9561                    formualizer_common::ResourceExhaustionReason::ScratchMemory,
9562                    0,
9563                    points.len() as u64 * 16,
9564                )
9565            })?;
9566        }
9567
9568        let estimated_commit_duration = std::time::Duration::from_nanos(
9569            (new_vertices as u64)
9570                .saturating_add(new_edges as u64)
9571                .saturating_add(prepared.len() as u64)
9572                .saturating_add(selected_package_records)
9573                .max(1)
9574                .saturating_mul(100),
9575        );
9576        if options.deadline.is_some_and(|deadline| {
9577            std::time::Instant::now()
9578                .checked_add(estimated_commit_duration)
9579                .is_none_or(|finish| finish > deadline)
9580        }) {
9581            self.observe_target_admission_failure(
9582                formualizer_common::ResourceExhaustionReason::Deadline,
9583            );
9584            return Err(resource(
9585                formualizer_common::ResourceExhaustionReason::Deadline,
9586                0,
9587                1,
9588            ));
9589        }
9590        #[cfg(test)]
9591        if let Some(hook) = self.before_target_preparation_commit_hook.take() {
9592            hook();
9593        }
9594        self.target_preparation_checkpoint(options.deadline, 0)?;
9595        #[cfg(test)]
9596        self.target_preparation_fault(
9597            crate::engine::target_preparation::TargetPreparationFault::FinalRevisionValidation,
9598        )?;
9599        let current_revisions = self.preparation_revisions();
9600        let staged_leases_match = prepared.iter().all(|formula| {
9601            self.staged_formula_index
9602                .lease_matches(&formula.sheet, formula.lease)
9603        }) && prepared_packages.iter().all(|package| {
9604            self.staged_formula_index
9605                .package_lease_matches(&package.sheet, package.lease)
9606        });
9607        let stale_reason = Self::preparation_revision_stale_reason(
9608            &assumptions,
9609            &current_revisions,
9610            &planning_requests,
9611            staged_leases_match,
9612        );
9613        if let Some(reason) = stale_reason {
9614            return Err(Self::preparation_stale(
9615                reason,
9616                "target graph preparation plan is stale",
9617            ));
9618        }
9619        #[cfg(test)]
9620        self.target_preparation_fault(
9621            crate::engine::target_preparation::TargetPreparationFault::FinalGraphValidation,
9622        )?;
9623        self.graph
9624            .validate_prepared_legacy_graph_plan(&legacy_graph)
9625            .map_err(|error| {
9626                Self::preparation_stale(
9627                    formualizer_common::PreparationStaleReason::Graph,
9628                    format!("target graph preparation plan is stale: {error}"),
9629                )
9630            })?;
9631        #[cfg(test)]
9632        self.target_preparation_fault(
9633            crate::engine::target_preparation::TargetPreparationFault::Reservation,
9634        )?;
9635        self.graph.reserve_prepared_legacy_graph_plan(&legacy_graph);
9636        self.formula_parse_diagnostics
9637            .try_reserve(pending_diagnostics.len())
9638            .map_err(|_| {
9639                resource(
9640                    formualizer_common::ResourceExhaustionReason::Admission,
9641                    pending_diagnostics.len() as u64,
9642                    pending_diagnostics.len() as u64,
9643                )
9644            })?;
9645        self.target_preparation_checkpoint(options.deadline, 0)?;
9646        #[cfg(test)]
9647        self.target_preparation_fault(
9648            crate::engine::target_preparation::TargetPreparationFault::BeforeFirstMutation,
9649        )?;
9650
9651        let commit_started = crate::instant::FzInstant::now();
9652        let committed = self
9653            .graph
9654            .apply_prevalidated_legacy_graph_plan(legacy_graph);
9655        for formula in &prepared {
9656            let removed = self
9657                .staged_formulas
9658                .get_mut(&formula.sheet)
9659                .and_then(|sheet| sheet.remove_ordinary(formula.lease.row, formula.lease.col));
9660            debug_assert!(removed.is_some());
9661            let index_removed = self.staged_formula_index.remove(
9662                &formula.sheet,
9663                formula.lease.row,
9664                formula.lease.col,
9665            );
9666            debug_assert!(index_removed);
9667        }
9668        for package in &prepared_packages {
9669            if let Some(points) = &package.selected_points {
9670                let staged = self.staged_formulas.get_mut(&package.sheet).unwrap();
9671                let source = staged.deferred_package.as_mut().unwrap();
9672                source.consumed_engine = Some(Arc::clone(&self.source_formula_token));
9673                // The revision-validated graph commit establishes this exact source
9674                // ownership proof. Later edits retain the exclusion, not old text.
9675                source
9676                    .consumed_members
9677                    .extend(package.replay_records.iter().filter_map(|record| {
9678                        record
9679                            .partition_owner
9680                            .or(record.family)
9681                            .filter(|owner| residual_owners[&package.sheet].contains(owner))
9682                            .map(|owner| {
9683                                (
9684                                    owner,
9685                                    crate::engine::SourceCoord {
9686                                        row: record.row - 1,
9687                                        col: record.col - 1,
9688                                    },
9689                                )
9690                            })
9691                    }));
9692                source.suppressed.extend(points.iter().copied());
9693                source.source_accounted = true;
9694                self.staged_formula_index
9695                    .consume_package_points(&package.sheet, points);
9696                if source.suppressed.len() >= source.source_coordinates.len() {
9697                    staged.deferred_package = None;
9698                    self.staged_formula_index.set_package(&package.sheet, None);
9699                }
9700            } else {
9701                let removed = self
9702                    .staged_formulas
9703                    .get_mut(&package.sheet)
9704                    .and_then(|staged| staged.deferred_package.take());
9705                debug_assert!(removed.is_some());
9706                self.staged_formula_index.set_package(&package.sheet, None);
9707            }
9708        }
9709        for (sheet, (families, partitions)) in residual_sources {
9710            if let Some(source) = self
9711                .staged_formulas
9712                .get_mut(&sheet)
9713                .and_then(|staged| staged.deferred_package.as_mut())
9714            {
9715                source.families = families;
9716                source.partitioned_families = partitions;
9717                source
9718                    .consumed_members
9719                    .retain(|(owner, _)| residual_owners[&sheet].contains(owner));
9720                self.staged_formula_index.update_package_family_count(
9721                    &sheet,
9722                    source.families.len() + source.partitioned_families.len(),
9723                );
9724            }
9725        }
9726        let empty_sheets = self
9727            .staged_formulas
9728            .iter()
9729            .filter_map(|(sheet, staged)| staged.is_empty().then_some(sheet.clone()))
9730            .collect::<Vec<_>>();
9731        for sheet in empty_sheets {
9732            self.staged_formulas.remove(&sheet);
9733        }
9734        if committed > 0 {
9735            self.mark_topology_edited();
9736        }
9737        self.formula_parse_diagnostics.extend(pending_diagnostics);
9738        if !prepared.is_empty() || !prepared_packages.is_empty() {
9739            let mut ingest_delta = FormulaIngestReport::with_mode(FormulaPlaneMode::Off);
9740            ingest_delta.formula_cells_seen = (prepared.len() as u64).saturating_add(
9741                prepared_packages
9742                    .iter()
9743                    .map(|package| {
9744                        (package.replay_records.len() as u64).saturating_add(
9745                            if package.selected_points.is_some() {
9746                                package.direct_cells
9747                            } else {
9748                                0
9749                            },
9750                        )
9751                    })
9752                    .sum::<u64>(),
9753            );
9754            ingest_delta.graph_formula_cells_materialized = committed as u64;
9755            ingest_delta.graph_vertices_created = new_vertices as u64;
9756            ingest_delta.graph_edges_created = new_edges as u64;
9757            for package in &prepared_packages {
9758                let source = &package.source_report;
9759                ingest_delta.source_formula_events = ingest_delta
9760                    .source_formula_events
9761                    .saturating_add(source.source_formula_events);
9762                ingest_delta.source_formula_records_spooled = ingest_delta
9763                    .source_formula_records_spooled
9764                    .saturating_add(source.source_formula_records_spooled);
9765                ingest_delta.source_spool_encoded_bytes = ingest_delta
9766                    .source_spool_encoded_bytes
9767                    .saturating_add(source.source_spool_encoded_bytes);
9768                ingest_delta.source_spool_peak_memory_bytes = ingest_delta
9769                    .source_spool_peak_memory_bytes
9770                    .max(source.source_spool_peak_memory_bytes);
9771                ingest_delta.source_spool_spilled_bytes = ingest_delta
9772                    .source_spool_spilled_bytes
9773                    .saturating_add(source.source_spool_spilled_bytes);
9774                ingest_delta.source_spool_spill_files = ingest_delta
9775                    .source_spool_spill_files
9776                    .saturating_add(source.source_spool_spill_files);
9777                ingest_delta.source_spool_replays = ingest_delta
9778                    .source_spool_replays
9779                    .saturating_add(source.source_spool_replays)
9780                    .saturating_add(package.spool_replays);
9781                ingest_delta.source_families_seen = ingest_delta
9782                    .source_families_seen
9783                    .saturating_add(source.families_seen);
9784                ingest_delta.source_family_cells_seen = ingest_delta
9785                    .source_family_cells_seen
9786                    .saturating_add(source.family_cells_seen);
9787                ingest_delta.source_family_shadow_eligible = ingest_delta
9788                    .source_family_shadow_eligible
9789                    .saturating_add(source.source_clean_families);
9790                ingest_delta.source_family_shadow_eligible_cells = ingest_delta
9791                    .source_family_shadow_eligible_cells
9792                    .saturating_add(source.source_clean_cells);
9793                ingest_delta.source_partitioned_families_seen = ingest_delta
9794                    .source_partitioned_families_seen
9795                    .saturating_add(source.source_fragmentable_families);
9796                ingest_delta.source_partition_holes = ingest_delta
9797                    .source_partition_holes
9798                    .saturating_add(source.source_hole_exclusions);
9799                ingest_delta.source_partition_ordinary_exceptions = ingest_delta
9800                    .source_partition_ordinary_exceptions
9801                    .saturating_add(source.source_ordinary_exclusions);
9802                ingest_delta.source_partition_surviving_cells = ingest_delta
9803                    .source_partition_surviving_cells
9804                    .saturating_add(source.source_fragmentable_cells);
9805                for (reason, count) in &source.fallback_reasons {
9806                    let total = ingest_delta
9807                        .fallback_reasons
9808                        .entry(reason.clone())
9809                        .or_default();
9810                    *total = total.saturating_add(*count);
9811                }
9812
9813                ingest_delta.source_family_fallback = ingest_delta
9814                    .source_family_fallback
9815                    .saturating_add(source.families_seen);
9816                ingest_delta.source_family_fallback_cells = ingest_delta
9817                    .source_family_fallback_cells
9818                    .saturating_add(source.family_cells_seen);
9819            }
9820            self.record_formula_ingest_report(ingest_delta);
9821        }
9822        let commit_window = commit_started.elapsed();
9823        let retained_cells = self
9824            .staged_formula_index
9825            .all_leases()
9826            .into_iter()
9827            .filter_map(|(sheet, lease)| {
9828                formualizer_common::RangeAddress::new(
9829                    sheet, lease.row, lease.col, lease.row, lease.col,
9830                )
9831                .ok()
9832            })
9833            .collect::<Vec<_>>();
9834        let observed_scratch_bytes = self
9835            .active_resource_ledger
9836            .as_ref()
9837            .map_or(0, |ledger| ledger.snapshot().scratch_peak)
9838            .saturating_sub(ledger_at_start.map_or(0, |snapshot| snapshot.scratch_current));
9839        let committed_spans = 0u64;
9840        let selected_source_families = prepared_packages
9841            .iter()
9842            .filter(|package| package.selected_points.is_none())
9843            .map(|package| package.lease.family_count)
9844            .sum::<usize>()
9845            + prepared_packages
9846                .iter()
9847                .filter(|package| package.selected_points.is_some())
9848                .flat_map(|package| package.replay_records.iter())
9849                .filter_map(|record| record.partition_owner.or(record.family))
9850                .chain(
9851                    prepared_packages
9852                        .iter()
9853                        .flat_map(|package| package.complete_selections.iter().copied()),
9854                )
9855                .collect::<BTreeSet<_>>()
9856                .len();
9857        let selected_staged_cells = prepared.len().saturating_add(
9858            prepared_packages
9859                .iter()
9860                .map(|package| {
9861                    package
9862                        .selected_points
9863                        .as_ref()
9864                        .map_or(package.replay_records.len(), BTreeSet::len)
9865                })
9866                .sum::<usize>(),
9867        );
9868        let actual_commit_work = (new_vertices as u64)
9869            .saturating_add(new_edges as u64)
9870            .saturating_add(committed as u64)
9871            .saturating_add(committed_spans)
9872            .saturating_add(prepared.len() as u64)
9873            .saturating_add(
9874                prepared_packages
9875                    .iter()
9876                    .map(|package| {
9877                        package
9878                            .selected_points
9879                            .as_ref()
9880                            .map_or(1, |points| points.len() as u64)
9881                    })
9882                    .sum::<u64>(),
9883            );
9884        let report = PreparedTargetGraphReport {
9885            request_id: request_id.unwrap_or_default(),
9886            requested_targets: targets.len(),
9887            normalized_regions: visited_regions.len(),
9888            normalized_target_list: normalized,
9889            selected_staged_cells,
9890            selected_source_families,
9891            retained_staged_cells: self.staged_formula_count(),
9892            selected_cells,
9893            retained_cells,
9894            widened_scope: scope,
9895            widening_reasons: reasons,
9896            revisions: assumptions,
9897            commit_window,
9898            estimated_scratch_bytes: scratch_bytes,
9899            observed_scratch_bytes,
9900            estimated_commit_work: (new_vertices as u64)
9901                .saturating_add(new_edges as u64)
9902                .saturating_add(prepared.len() as u64)
9903                .saturating_add(selected_package_records),
9904            actual_commit_work,
9905            outcome: PreparationOutcome::Prepared,
9906        };
9907        self.observe_target_preparation_report(&report);
9908        Ok(report)
9909    }
9910
9911    /// Build graph for all staged formulas.
9912    pub fn build_graph_all(&mut self) -> Result<(), formualizer_parse::ExcelError> {
9913        self.observe_evaluation_resource_request(EvaluationRequestKind::Full, |engine| {
9914            engine.build_graph_all_unobserved()
9915        })
9916    }
9917
9918    fn build_graph_all_unobserved(&mut self) -> Result<(), formualizer_parse::ExcelError> {
9919        let selected = self.staged_formula_count();
9920        let started = crate::instant::FzInstant::now();
9921        self.resource_checkpoint(selected as u64)?;
9922        let scratch_bytes = (selected as u64).saturating_mul(256);
9923        let result = self.with_request_scratch(scratch_bytes, |engine| {
9924            let index_snapshot = engine.staged_formula_index.clone();
9925            let collected = std::mem::take(&mut engine.staged_formulas)
9926                .into_iter()
9927                .collect();
9928            engine.staged_formula_index.clear_all();
9929            engine.build_graph_from_staged_batches(collected, false, index_snapshot)
9930        });
9931        self.observe_staged_preparation(selected, self.staged_formula_count(), started.elapsed());
9932        result
9933    }
9934
9935    /// Build graph for specific sheets (consuming only those staged entries).
9936    pub fn build_graph_for_sheets<'a, I: IntoIterator<Item = &'a str>>(
9937        &mut self,
9938        sheets: I,
9939    ) -> Result<(), formualizer_parse::ExcelError> {
9940        let mut sheets = sheets.into_iter();
9941        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, move |engine| {
9942            let name_scratch = (sheets.size_hint().0 as u64).saturating_mul(64);
9943            engine.with_request_scratch(name_scratch, |engine| {
9944                // Allocation failure follows the baseline process-fatal policy; it is not a
9945                // recoverable resource error or a new staged-preparation route.
9946                let names = sheets.by_ref().map(str::to_string).collect::<Vec<_>>();
9947                engine.charge_bounded_work(names.len() as u64)?;
9948                engine.build_graph_for_sheet_names_unobserved(names)
9949            })
9950        })
9951    }
9952
9953    fn build_graph_for_sheet_names_unobserved(
9954        &mut self,
9955        sheets: Vec<String>,
9956    ) -> Result<(), formualizer_parse::ExcelError> {
9957        let started = crate::instant::FzInstant::now();
9958        let selected = sheets
9959            .iter()
9960            .filter_map(|sheet| self.staged_formulas.get(sheet))
9961            .map(StagedSheet::len)
9962            .sum::<usize>();
9963        self.resource_checkpoint(selected as u64)?;
9964        let scratch_bytes = (selected as u64).saturating_mul(256);
9965        self.reserve_request_scratch(scratch_bytes)?;
9966        let index_snapshot = self.staged_formula_index.clone();
9967        let mut collected = Vec::new();
9968        for sheet in sheets {
9969            if let Some(staged) = self.staged_formulas.remove(&sheet) {
9970                self.index_removed_staged_sheet(&sheet, &staged);
9971                collected.push((sheet, staged));
9972            }
9973        }
9974        let result = self.build_graph_from_staged_batches(collected, true, index_snapshot);
9975        self.release_request_scratch(scratch_bytes);
9976        self.observe_staged_preparation(selected, self.staged_formula_count(), started.elapsed());
9977        result
9978    }
9979
9980    fn build_graph_from_staged_batches(
9981        &mut self,
9982        collected: StagedFormulaBatches,
9983        share_parse_cache_across_sheets: bool,
9984        staged_index_snapshot: StagedFormulaIndex,
9985    ) -> Result<(), formualizer_parse::ExcelError> {
9986        if collected.is_empty() {
9987            return Ok(());
9988        }
9989        for (sheet, _) in &collected {
9990            let _ = self.add_sheet(sheet);
9991        }
9992
9993        let diagnostics_len = self.formula_parse_diagnostics.len();
9994        let mut collected = collected;
9995        let prepared = match self
9996            .prepare_staged_formula_batches(&mut collected, share_parse_cache_across_sheets)
9997        {
9998            Ok(prepared) => prepared,
9999            Err(error) => {
10000                self.formula_parse_diagnostics.truncate(diagnostics_len);
10001                for (sheet, staged) in collected {
10002                    self.restore_staged_sheet(sheet, staged);
10003                }
10004                self.staged_formula_index = staged_index_snapshot;
10005                return Err(error);
10006            }
10007        };
10008        let (ordinary, compressed, direct, may_fail) = prepared;
10009
10010        // A first build (no formula in the graph yet) goes through the eager
10011        // first load's machinery: the builder pre-allocates each column's
10012        // targets as one id run, installs family members as virtual runs and
10013        // builds the authority once at the end (decision 26).
10014        // The first load's builder plans and applies one chunk at a time, so
10015        // a planning error would leave earlier chunks in the graph; the
10016        // incremental path plans everything first. Check every distinct
10017        // formula first, in the incremental path's order (it reports the
10018        // same first error and leaves the graph untouched).
10019        let first_build = if self.deferred_build_can_be_first_load() {
10020            if let Err(error) = self.check_staged_formula_plans(
10021                ordinary
10022                    .iter()
10023                    .chain(compressed.iter().map(|(batch, _)| batch)),
10024                &may_fail,
10025            ) {
10026                self.formula_parse_diagnostics.truncate(diagnostics_len);
10027                for (sheet, staged) in collected {
10028                    self.restore_staged_sheet(sheet, staged);
10029                }
10030                self.staged_formula_index = staged_index_snapshot;
10031                return Err(error);
10032            }
10033            self.deferred_build_as_first_load()
10034        } else {
10035            None
10036        };
10037        let built_sheets: Vec<String> = if first_build.is_some() {
10038            collected.iter().map(|(sheet, _)| sheet.clone()).collect()
10039        } else {
10040            Vec::new()
10041        };
10042
10043        // Keep the original source/spool alive through every fallible ingestion route.
10044        // Graph admission may have committed a prefix; replay replaces those placements
10045        // rather than treating their cached values as authoritative source.
10046        let result = (|| {
10047            if !ordinary.is_empty() {
10048                self.ingest_formula_batches(ordinary)?;
10049            }
10050            if !compressed.is_empty() {
10051                self.ingest_compressed_formula_source_batches(compressed)?;
10052            }
10053            if !direct.is_empty() {
10054                self.finish_compressed_formula_sources(direct)?;
10055            }
10056            Ok(())
10057        })();
10058        if let Some(saved) = first_build {
10059            self.leave_deferred_first_load(saved, &built_sheets);
10060        }
10061        if let Err(error) = result {
10062            self.formula_parse_diagnostics.truncate(diagnostics_len);
10063            for (sheet, staged) in collected {
10064                self.restore_staged_sheet(sheet, staged);
10065            }
10066            self.staged_formula_index = staged_index_snapshot;
10067            return Err(error);
10068        }
10069        self.dedup_formula_parse_diagnostics_since(diagnostics_len);
10070        Ok(())
10071    }
10072
10073    /// Enter the first-load ingest mode for a deferred build when the graph
10074    /// holds no formula yet and no load is in progress: the settings the
10075    /// calamine loader uses for its eager first load (lazy sheet index, no
10076    /// small-range expansion, first-load fast path). Returns the settings
10077    /// to restore, or `None` when the build takes the incremental path.
10078    fn deferred_build_can_be_first_load(&self) -> bool {
10079        !self.graph.first_load_assume_new()
10080            && !self.graph_admission_enabled()
10081            && self.graph.formula_vertex_count() == 0
10082    }
10083
10084    /// Plan each distinct formula of `batches` that may fail planning once
10085    /// (a member is planned through its template, staged before it) and
10086    /// drop the plans: the first planning error, in the order the
10087    /// incremental ingest meets it.
10088    fn check_staged_formula_plans<'b>(
10089        &mut self,
10090        batches: impl Iterator<Item = &'b FormulaIngestBatch>,
10091        may_fail: &FxHashSet<crate::engine::arena::AstNodeId>,
10092    ) -> Result<(), ExcelError> {
10093        if may_fail.is_empty() {
10094            return Ok(());
10095        }
10096        let mut seen: FxHashSet<(SheetId, crate::engine::arena::AstNodeId)> = FxHashSet::default();
10097        for batch in batches {
10098            let sheet_id = self.graph.sheet_id(&batch.sheet_name).ok_or_else(|| {
10099                ExcelError::new(ExcelErrorKind::Ref)
10100                    .with_message(format!("unknown ingest sheet: {}", batch.sheet_name))
10101            })?;
10102            let mut pipeline = self.ingest_pipeline();
10103            for record in &batch.formulas {
10104                if record.member_anchor.is_some()
10105                    || !may_fail.contains(&record.ast_id)
10106                    || !seen.insert((sheet_id, record.ast_id))
10107                {
10108                    continue;
10109                }
10110                let placement = CellRef::new(
10111                    sheet_id,
10112                    Coord::from_excel(record.row, record.col, true, true),
10113                );
10114                pipeline.ingest_formula(
10115                    FormulaAstInput::RawArena(record.ast_id),
10116                    placement,
10117                    None,
10118                )?;
10119            }
10120        }
10121        Ok(())
10122    }
10123
10124    fn deferred_build_as_first_load(&mut self) -> Option<(crate::engine::SheetIndexMode, usize)> {
10125        if !self.deferred_build_can_be_first_load() {
10126            return None;
10127        }
10128        let saved = (
10129            self.graph.get_config().sheet_index_mode,
10130            self.config.range_expansion_limit,
10131        );
10132        self.graph
10133            .set_sheet_index_mode(crate::engine::SheetIndexMode::Lazy);
10134        self.config.range_expansion_limit = 0;
10135        self.graph.set_first_load_assume_new(true);
10136        self.graph.reset_ensure_touched();
10137        Some(saved)
10138    }
10139
10140    /// Leave the first-load mode of [`Self::deferred_build_as_first_load`]:
10141    /// the authority is built once here, as at the end of an eager load.
10142    fn leave_deferred_first_load(
10143        &mut self,
10144        (index_mode, range_limit): (crate::engine::SheetIndexMode, usize),
10145        sheets: &[String],
10146    ) {
10147        self.graph.set_first_load_assume_new(false);
10148        self.graph.reset_ensure_touched();
10149        self.graph.set_sheet_index_mode(index_mode);
10150        self.config.range_expansion_limit = range_limit;
10151        for sheet in sheets {
10152            self.graph.finalize_sheet_index(sheet);
10153        }
10154    }
10155
10156    fn prepare_staged_formula_batches(
10157        &mut self,
10158        collected: &mut StagedFormulaBatches,
10159        share_parse_cache_across_sheets: bool,
10160    ) -> Result<PreparedStagedFormulaBatches, formualizer_parse::ExcelError> {
10161        let mut ordinary = Vec::new();
10162        let mut compressed = Vec::new();
10163        let mut direct = Vec::new();
10164        let mut may_fail = FxHashSet::default();
10165        let mut cache: rustc_hash::FxHashMap<String, Option<crate::engine::arena::AstNodeId>> =
10166            rustc_hash::FxHashMap::default();
10167        cache.reserve(4096);
10168
10169        for (sheet, staged) in collected {
10170            if !share_parse_cache_across_sheets {
10171                cache.clear();
10172            }
10173            // Load-time family grouping, as the eager first load does:
10174            // relative copies of the formula above (or to the left) become
10175            // members of its family and are never interned.
10176            let mut grouper = crate::engine::FormulaFamilyGrouper::new();
10177            // The staged texts, then the deferred package's replayed ones,
10178            // are walked once (no combined copy: a replay can be the whole
10179            // sheet).
10180            let mut replayed_records = Vec::new();
10181            let deferred_source = None;
10182            let mut deferred_fallback = None;
10183            if let Some(package) = staged.deferred_package.as_mut() {
10184                if package.sheet_name != *sheet {
10185                    return Err(ExcelError::new(ExcelErrorKind::Value)
10186                        .with_message("deferred formula package sheet mismatch"));
10187                }
10188                let eligible: Vec<_> = package
10189                    .families
10190                    .iter()
10191                    .filter(|family| !package.invalidated.contains(&family.source_id))
10192                    .cloned()
10193                    .collect();
10194                let eligible_partitions: Vec<_> = package
10195                    .partitioned_families
10196                    .iter()
10197                    .filter(|family| !package.invalidated.contains(&family.source_id))
10198                    .cloned()
10199                    .collect();
10200
10201                let mut replay_disposition = crate::engine::FormulaReplayDisposition::default();
10202                for partition in &eligible_partitions {
10203                    replay_disposition
10204                        .register_partition(partition, false)
10205                        .map_err(|reason| {
10206                            ExcelError::new(ExcelErrorKind::Value).with_message(reason)
10207                        })?;
10208                }
10209                replay_disposition
10210                    .extend_suppressed_excel_coords(package.suppressed.iter().copied());
10211                let replayed = package
10212                    .replay
10213                    .lock()
10214                    .map_err(|_| {
10215                        ExcelError::new(ExcelErrorKind::Value)
10216                            .with_message("deferred formula spool lock poisoned")
10217                    })?
10218                    .replay(&replay_disposition)
10219                    .map_err(|message| {
10220                        ExcelError::new(ExcelErrorKind::Value).with_message(message)
10221                    })?;
10222                replayed_records = replayed;
10223                let mut report = package.accounting_report();
10224                report.source_spool_replays = report.source_spool_replays.saturating_add(1);
10225                deferred_fallback = Some((report, package.families.clone(), eligible_partitions));
10226            }
10227
10228            let n_entries = staged.entries.len() + replayed_records.len();
10229            let entries = staged
10230                .entries
10231                .iter()
10232                .cloned()
10233                .map(|(row, col, text)| (row, col, text, None))
10234                .chain(replayed_records.into_iter().map(|record| {
10235                    (
10236                        record.row,
10237                        record.col,
10238                        record.text,
10239                        Some((record.source_order, record.family, record.partition_owner)),
10240                    )
10241                }));
10242            let mut formulas = Vec::with_capacity(n_entries);
10243            let staged_order_base = u64::MAX.saturating_sub(n_entries as u64);
10244            for (entry_index, (row, col, txt, source_proof)) in entries.enumerate() {
10245                let key = if txt.starts_with('=') {
10246                    txt
10247                } else {
10248                    format!("={txt}")
10249                };
10250                let staged_record = if let Some(cached) = cache.get(&key) {
10251                    cached.map(|ast_id| {
10252                        self.note_staged_formula(&mut grouper, row, col, ast_id);
10253                        FormulaIngestRecord::new(row, col, ast_id, Some(Arc::<str>::from(key)))
10254                    })
10255                } else {
10256                    let parsed = match formualizer_parse::parser::parse(&key) {
10257                        Ok(parsed) => Some(parsed),
10258                        Err(error) => self.handle_formula_parse_error(
10259                            sheet,
10260                            row,
10261                            col,
10262                            &key,
10263                            error.to_string(),
10264                        )?,
10265                    };
10266                    match parsed {
10267                        Some(ast) => {
10268                            let record = self.stage_formula_ast(&mut grouper, row, col, &ast, None);
10269                            if !record.is_family_member() && self.formula_may_fail_planning(&ast) {
10270                                may_fail.insert(record.ast_id);
10271                            }
10272                            // A member's text is not worth caching: relative
10273                            // copies do not repeat their text.
10274                            if record.is_family_member() {
10275                                Some(record)
10276                            } else {
10277                                let ast_id = record.ast_id;
10278                                cache.insert(key.clone(), Some(ast_id));
10279                                Some(FormulaIngestRecord::new(
10280                                    row,
10281                                    col,
10282                                    ast_id,
10283                                    Some(Arc::<str>::from(key)),
10284                                ))
10285                            }
10286                        }
10287                        None => {
10288                            cache.insert(key, None);
10289                            None
10290                        }
10291                    }
10292                };
10293
10294                if let Some(mut formula) = staged_record {
10295                    if let Some((order, family, owner)) = source_proof {
10296                        formula = formula.with_source_proof(order, family, owner);
10297                    } else if deferred_source.is_some() {
10298                        formula = formula.with_source_proof(
10299                            crate::engine::SourceFormulaOrder::new(
10300                                staged_order_base.saturating_add(entry_index as u64),
10301                            ),
10302                            None,
10303                            None,
10304                        );
10305                    }
10306                    formulas.push(formula);
10307                }
10308            }
10309
10310            let batch = FormulaIngestBatch::new(sheet.clone(), formulas);
10311            if let Some((report, preparation)) = deferred_source {
10312                direct.push((batch, report, preparation));
10313            } else if let Some((report, families, partitions)) = deferred_fallback {
10314                let source_batch = crate::engine::FormulaCompressedSourceBatch::with_proposals(
10315                    batch.sheet_name.clone(),
10316                    report,
10317                    families,
10318                    partitions,
10319                );
10320                compressed.push((batch, source_batch));
10321            } else if !batch.is_empty() {
10322                ordinary.push(batch);
10323            }
10324        }
10325        Ok((ordinary, compressed, direct, may_fail))
10326    }
10327
10328    /// Whether planning `ast` can fail: a reference to a sheet that does not
10329    /// exist, an external, 3-D or table reference, or a reversed range.
10330    /// Unqualified cell and range references, names, literals and calls
10331    /// always plan.
10332    fn formula_may_fail_planning(&self, ast: &formualizer_parse::parser::ASTNode) -> bool {
10333        use formualizer_parse::parser::{ASTNodeType, ReferenceType};
10334        let sheet_missing = |sheet: &Option<String>| {
10335            sheet
10336                .as_deref()
10337                .is_some_and(|s| self.graph.sheet_id(s).is_none())
10338        };
10339        match &ast.node_type {
10340            ASTNodeType::Literal(_) | ASTNodeType::Omitted => false,
10341            ASTNodeType::Reference { reference, .. } => match reference {
10342                ReferenceType::Cell { sheet, .. } => sheet_missing(sheet),
10343                ReferenceType::Range {
10344                    sheet,
10345                    start_row,
10346                    start_col,
10347                    end_row,
10348                    end_col,
10349                    ..
10350                } => {
10351                    sheet_missing(sheet)
10352                        || matches!((start_row, end_row), (Some(a), Some(b)) if a > b)
10353                        || matches!((start_col, end_col), (Some(a), Some(b)) if a > b)
10354                }
10355                ReferenceType::NamedRange(_) => false,
10356                _ => true,
10357            },
10358            ASTNodeType::UnaryOp { expr, .. } => self.formula_may_fail_planning(expr),
10359            ASTNodeType::BinaryOp { left, right, .. } => {
10360                self.formula_may_fail_planning(left) || self.formula_may_fail_planning(right)
10361            }
10362            ASTNodeType::Function { args, .. } => {
10363                args.iter().any(|a| self.formula_may_fail_planning(a))
10364            }
10365            ASTNodeType::Call { callee, args } => {
10366                self.formula_may_fail_planning(callee)
10367                    || args.iter().any(|a| self.formula_may_fail_planning(a))
10368            }
10369            ASTNodeType::Array(rows) => rows
10370                .iter()
10371                .flatten()
10372                .any(|a| self.formula_may_fail_planning(a)),
10373        }
10374    }
10375
10376    /// Begin bulk Arrow ingest for base values (Phase A)
10377    pub fn begin_bulk_ingest_arrow(
10378        &mut self,
10379    ) -> crate::engine::arrow_ingest::ArrowBulkIngestBuilder<'_, R> {
10380        crate::engine::arrow_ingest::ArrowBulkIngestBuilder::new(self)
10381    }
10382
10383    /// Begin bulk updates to Arrow store (Phase C)
10384    pub fn begin_bulk_update_arrow(
10385        &mut self,
10386    ) -> crate::engine::arrow_ingest::ArrowBulkUpdateBuilder<'_, R> {
10387        crate::engine::arrow_ingest::ArrowBulkUpdateBuilder::new(self)
10388    }
10389
10390    fn ensure_known_sheet_id(&self, sheet: &str) -> Result<SheetId, crate::engine::EditorError> {
10391        self.graph.sheet_id(sheet).ok_or(
10392            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
10393                name: sheet.to_string(),
10394                reason: "Unknown sheet".to_string(),
10395            },
10396        )
10397    }
10398
10399    fn normalize_row_1based(row_1based: u32) -> Result<u32, crate::engine::EditorError> {
10400        if row_1based == 0 {
10401            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
10402        }
10403        Ok(row_1based - 1)
10404    }
10405
10406    fn normalize_row_range_1based(
10407        start_row_1based: u32,
10408        end_row_1based: u32,
10409    ) -> Result<(u32, u32), crate::engine::EditorError> {
10410        if start_row_1based == 0 || end_row_1based == 0 {
10411            return Err(crate::engine::EditorError::OutOfBounds { row: 0, col: 0 });
10412        }
10413        if start_row_1based > end_row_1based {
10414            return Err(crate::engine::EditorError::TransactionFailed {
10415                reason: "Row range start is greater than end".to_string(),
10416            });
10417        }
10418        Ok((start_row_1based - 1, end_row_1based - 1))
10419    }
10420
10421    fn invalidate_row_visibility_mask_cache(&self) {
10422        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
10423            cache.clear();
10424        }
10425    }
10426
10427    fn set_row_hidden_by_sheet_id(
10428        &mut self,
10429        sheet_id: SheetId,
10430        row0: u32,
10431        hidden: bool,
10432        source: RowVisibilitySource,
10433    ) -> bool {
10434        let changed = {
10435            let state = self.row_visibility.entry(sheet_id).or_default();
10436            state.set_row_hidden(row0, hidden, source)
10437        };
10438
10439        let remove_entry = self
10440            .row_visibility
10441            .get(&sheet_id)
10442            .map(|state| state.is_empty())
10443            .unwrap_or(false);
10444        if remove_entry {
10445            self.row_visibility.remove(&sheet_id);
10446        }
10447
10448        if changed {
10449            self.invalidate_row_visibility_mask_cache();
10450        }
10451
10452        changed
10453    }
10454
10455    fn set_rows_hidden_by_sheet_id(
10456        &mut self,
10457        sheet_id: SheetId,
10458        start_row0: u32,
10459        end_row0: u32,
10460        hidden: bool,
10461        source: RowVisibilitySource,
10462    ) -> bool {
10463        let changed = {
10464            let state = self.row_visibility.entry(sheet_id).or_default();
10465            state.set_rows_hidden(start_row0, end_row0, hidden, source)
10466        };
10467
10468        let remove_entry = self
10469            .row_visibility
10470            .get(&sheet_id)
10471            .map(|state| state.is_empty())
10472            .unwrap_or(false);
10473        if remove_entry {
10474            self.row_visibility.remove(&sheet_id);
10475        }
10476
10477        if changed {
10478            self.invalidate_row_visibility_mask_cache();
10479        }
10480
10481        changed
10482    }
10483
10484    fn shift_row_visibility_insert(&mut self, sheet_id: SheetId, before0: u32, count: u32) {
10485        if count == 0 {
10486            return;
10487        }
10488        let mut changed = false;
10489        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
10490            changed = state.insert_rows(before0, count);
10491            state.is_empty()
10492        } else {
10493            false
10494        };
10495        if remove_entry {
10496            self.row_visibility.remove(&sheet_id);
10497        }
10498        if changed {
10499            self.invalidate_row_visibility_mask_cache();
10500        }
10501    }
10502
10503    fn shift_row_visibility_delete(&mut self, sheet_id: SheetId, start0: u32, count: u32) {
10504        if count == 0 {
10505            return;
10506        }
10507        let mut changed = false;
10508        let remove_entry = if let Some(state) = self.row_visibility.get_mut(&sheet_id) {
10509            changed = state.delete_rows(start0, count);
10510            state.is_empty()
10511        } else {
10512            false
10513        };
10514        if remove_entry {
10515            self.row_visibility.remove(&sheet_id);
10516        }
10517        if changed {
10518            self.invalidate_row_visibility_mask_cache();
10519        }
10520    }
10521
10522    fn apply_inverse_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
10523        if let crate::engine::ChangeEvent::SetRowVisibility {
10524            sheet_id,
10525            row0,
10526            source,
10527            old_hidden,
10528            ..
10529        } = event
10530        {
10531            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *old_hidden, *source);
10532        }
10533    }
10534
10535    fn apply_forward_row_visibility_event(&mut self, event: &crate::engine::ChangeEvent) {
10536        if let crate::engine::ChangeEvent::SetRowVisibility {
10537            sheet_id,
10538            row0,
10539            source,
10540            new_hidden,
10541            ..
10542        } = event
10543        {
10544            let _ = self.set_row_hidden_by_sheet_id(*sheet_id, *row0, *new_hidden, *source);
10545        }
10546    }
10547
10548    fn apply_inverse_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
10549        for event in events.iter().rev() {
10550            self.apply_inverse_row_visibility_event(event);
10551        }
10552    }
10553
10554    fn apply_forward_row_visibility_events(&mut self, events: &[crate::engine::ChangeEvent]) {
10555        for event in events {
10556            self.apply_forward_row_visibility_event(event);
10557        }
10558    }
10559
10560    fn apply_inverse_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
10561        if let crate::engine::ChangeEvent::StagedFormulaCellChanged {
10562            sheet,
10563            row,
10564            col,
10565            old,
10566            ..
10567        } = event
10568        {
10569            self.apply_staged_formula_cell(sheet, *row, *col, old.as_deref());
10570        }
10571    }
10572
10573    fn apply_forward_staged_formula_event(&mut self, event: &crate::engine::ChangeEvent) {
10574        if let crate::engine::ChangeEvent::StagedFormulaCellChanged {
10575            sheet,
10576            row,
10577            col,
10578            new,
10579            ..
10580        } = event
10581        {
10582            self.apply_staged_formula_cell(sheet, *row, *col, new.as_deref());
10583        }
10584    }
10585
10586    /// Set a single cell's staged formula text to `target` (clearing it when
10587    /// `None`). Used by undo/redo replay of per-cell staged-formula deltas.
10588    fn apply_staged_formula_cell(&mut self, sheet: &str, row: u32, col: u32, target: Option<&str>) {
10589        match target {
10590            Some(text) => self.stage_formula_text(sheet, row, col, text.to_string()),
10591            None => {
10592                self.clear_staged_formula_text(sheet, row, col);
10593            }
10594        }
10595    }
10596
10597    pub fn set_row_hidden(
10598        &mut self,
10599        sheet: &str,
10600        row_1based: u32,
10601        hidden: bool,
10602        source: RowVisibilitySource,
10603    ) -> Result<(), crate::engine::EditorError> {
10604        self.observe_function_semantic_epoch()
10605            .map_err(crate::engine::EditorError::Excel)?;
10606        self.observe_function_semantic_epoch()
10607            .map_err(crate::engine::EditorError::Excel)?;
10608        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10609        let row0 = Self::normalize_row_1based(row_1based)?;
10610        if self.set_row_hidden_by_sheet_id(sheet_id, row0, hidden, source) {
10611            self.record_structural_change(StructuralScope::Region(Region::whole_row(
10612                sheet_id, row0,
10613            )));
10614            self.mark_data_edited();
10615        }
10616        Ok(())
10617    }
10618
10619    pub fn set_rows_hidden(
10620        &mut self,
10621        sheet: &str,
10622        start_row_1based: u32,
10623        end_row_1based: u32,
10624        hidden: bool,
10625        source: RowVisibilitySource,
10626    ) -> Result<(), crate::engine::EditorError> {
10627        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10628        let (start_row0, end_row0) =
10629            Self::normalize_row_range_1based(start_row_1based, end_row_1based)?;
10630        if self.set_rows_hidden_by_sheet_id(sheet_id, start_row0, end_row0, hidden, source) {
10631            if start_row0 == end_row0 {
10632                self.record_structural_change(StructuralScope::Region(Region::whole_row(
10633                    sheet_id, start_row0,
10634                )));
10635            } else {
10636                self.record_structural_change(StructuralScope::Sheet(sheet_id));
10637            }
10638            self.mark_data_edited();
10639        }
10640        Ok(())
10641    }
10642
10643    pub fn is_row_hidden(
10644        &self,
10645        sheet: &str,
10646        row_1based: u32,
10647        source: Option<RowVisibilitySource>,
10648    ) -> Option<bool> {
10649        let sheet_id = self.graph.sheet_id(sheet)?;
10650        let row0 = row_1based.checked_sub(1)?;
10651        Some(
10652            self.row_visibility
10653                .get(&sheet_id)
10654                .map(|state| state.is_row_hidden(row0, source))
10655                .unwrap_or(false),
10656        )
10657    }
10658
10659    pub fn row_visibility_version(&self, sheet: &str) -> Option<u64> {
10660        let sheet_id = self.graph.sheet_id(sheet)?;
10661        Some(
10662            self.row_visibility
10663                .get(&sheet_id)
10664                .map(|state| state.version())
10665                .unwrap_or(0),
10666        )
10667    }
10668
10669    /// Cells of a sheet-backed `view` whose formula (own or a family
10670    /// template) calls SUBTOTAL, or AGGREGATE when `include_aggregate`, as
10671    /// sorted, disjoint `(col, first_row, last_row)` offset intervals within
10672    /// the view. Each formula's call bits are precomputed in the AST arena;
10673    /// a filled-down family is checked once per run, not once per cell.
10674    fn nested_subtotal_cells_for_view(
10675        &self,
10676        view: &RangeView<'_>,
10677        include_aggregate: bool,
10678    ) -> Option<Vec<(usize, usize, usize)>> {
10679        if !view.is_sheet_backed() {
10680            return None;
10681        }
10682        let (rows, cols) = view.dims();
10683        if rows == 0 || cols == 0 {
10684            return Some(Vec::new());
10685        }
10686        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
10687        let (r0, c0) = (view.start_row(), view.start_col());
10688        let mask = if include_aggregate {
10689            crate::engine::arena::SUBTOTAL_CALL | crate::engine::arena::AGGREGATE_CALL
10690        } else {
10691            crate::engine::arena::SUBTOTAL_CALL
10692        };
10693        let ds = self.graph.data_store();
10694        let mut found: Vec<(usize, usize, usize)> = self
10695            .graph
10696            .formula_intervals_in_region(
10697                sheet_id,
10698                r0 as u32,
10699                (r0 + rows - 1) as u32,
10700                c0 as u32,
10701                (c0 + cols - 1) as u32,
10702                |template| ds.ast_subtotal_calls(template) & mask != 0,
10703            )
10704            .into_iter()
10705            .map(|(col, lo, hi)| (col as usize - c0, lo as usize - r0, hi as usize - r0))
10706            .collect();
10707        found.sort_unstable();
10708        // Merge touching or overlapping intervals of a column so a binary
10709        // search finds the one covering a cell.
10710        let mut out: Vec<(usize, usize, usize)> = Vec::with_capacity(found.len());
10711        for (col, lo, hi) in found {
10712            match out.last_mut() {
10713                Some(last) if last.0 == col && lo <= last.2 + 1 => last.2 = last.2.max(hi),
10714                _ => out.push((col, lo, hi)),
10715            }
10716        }
10717        Some(out)
10718    }
10719
10720    fn build_row_visibility_mask_for_view(
10721        &self,
10722        view: &RangeView<'_>,
10723        mode: VisibilityMaskMode,
10724    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
10725        let sheet_rows = view.sheet().nrows as usize;
10726        if sheet_rows == 0 || view.start_row() >= sheet_rows {
10727            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
10728        }
10729
10730        let sheet_id = self.graph.sheet_id(view.sheet_name())?;
10731        let start_row0 = view.start_row() as u32;
10732        let end_row0 = view.end_row().min(sheet_rows.saturating_sub(1)) as u32;
10733        let version = self
10734            .row_visibility
10735            .get(&sheet_id)
10736            .map(|state| state.version())
10737            .unwrap_or(0);
10738        let key = VisibilityMaskCacheKey {
10739            sheet_id,
10740            start_row0,
10741            end_row0,
10742            mode,
10743            version,
10744        };
10745
10746        if let Ok(cache) = self.row_visibility_mask_cache.read()
10747            && let Some(mask) = cache.get(&key)
10748        {
10749            #[cfg(test)]
10750            visibility_mask_test_hooks::inc_hit();
10751            return Some(mask.clone());
10752        }
10753
10754        #[cfg(test)]
10755        visibility_mask_test_hooks::inc_miss();
10756
10757        let state = self.row_visibility.get(&sheet_id);
10758        let mut out = Vec::with_capacity((end_row0 - start_row0 + 1) as usize);
10759        for row0 in start_row0..=end_row0 {
10760            let manual_hidden = state
10761                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Manual)))
10762                .unwrap_or(false);
10763            let filter_hidden = state
10764                .map(|s| s.is_row_hidden(row0, Some(RowVisibilitySource::Filter)))
10765                .unwrap_or(false);
10766
10767            let include = match mode {
10768                VisibilityMaskMode::IncludeAll => true,
10769                VisibilityMaskMode::ExcludeManualHidden => !manual_hidden,
10770                VisibilityMaskMode::ExcludeFilterHidden => !filter_hidden,
10771                VisibilityMaskMode::ExcludeManualOrFilterHidden => {
10772                    !(manual_hidden || filter_hidden)
10773                }
10774            };
10775            out.push(include);
10776        }
10777
10778        let mask = std::sync::Arc::new(arrow_array::BooleanArray::from(out));
10779        if let Ok(mut cache) = self.row_visibility_mask_cache.write() {
10780            const MAX_CACHE_ENTRIES: usize = 4096;
10781            if cache.len() >= MAX_CACHE_ENTRIES {
10782                cache.clear();
10783                #[cfg(test)]
10784                visibility_mask_test_hooks::inc_eviction();
10785            }
10786            cache.insert(key, mask.clone());
10787        }
10788
10789        Some(mask)
10790    }
10791
10792    fn observe_function_semantic_epoch(&mut self) -> Result<bool, ExcelError> {
10793        let changes =
10794            crate::function_registry::semantic_changes_since(self.function_semantic_epoch_seen);
10795        let global_changed = changes.epoch != self.function_semantic_epoch_seen;
10796        let provider_revision = self.resolver.planning_semantic_revision();
10797        let provider_changed = provider_revision != self.function_provider_revision_seen;
10798        if !global_changed && !provider_changed {
10799            return Ok(false);
10800        }
10801
10802        let changed = !changes.keys.is_empty();
10803        if global_changed && changed || provider_changed {
10804            self.cached_static_schedule = None;
10805            self.recent_schedules.clear();
10806            self.base_schedule = None;
10807        }
10808        self.function_semantic_epoch_seen = changes.epoch;
10809        self.function_provider_revision_seen = provider_revision;
10810        Ok(false)
10811    }
10812
10813    pub(crate) fn ast_uses_changed_function(
10814        ast: &ASTNode,
10815        changed: &BTreeSet<(String, String)>,
10816    ) -> bool {
10817        match &ast.node_type {
10818            ASTNodeType::Function { name, args } => {
10819                let normalized = name.to_uppercase();
10820                let mut spellings = vec![(String::new(), normalized.clone())];
10821                let mut stripped = normalized.as_str();
10822                while let Some(rest) = ["_XLFN.", "_XLL.", "_XLWS."]
10823                    .iter()
10824                    .find_map(|prefix| stripped.strip_prefix(prefix))
10825                {
10826                    stripped = rest;
10827                    spellings.push((String::new(), stripped.to_string()));
10828                }
10829                let resolved = crate::function_registry::resolve("", name);
10830                let directly_changed = spellings.iter().any(|spelling| changed.contains(spelling))
10831                    || resolved.as_ref().is_some_and(|resolved| {
10832                        changed.contains(&(
10833                            resolved.namespace.clone(),
10834                            resolved.canonical_name.clone(),
10835                        ))
10836                    });
10837                directly_changed
10838                    || resolved.is_none()
10839                    || args
10840                        .iter()
10841                        .any(|arg| Self::ast_uses_changed_function(arg, changed))
10842            }
10843            ASTNodeType::Call { callee, args } => {
10844                Self::ast_uses_changed_function(callee, changed)
10845                    || args
10846                        .iter()
10847                        .any(|arg| Self::ast_uses_changed_function(arg, changed))
10848            }
10849            ASTNodeType::UnaryOp { expr, .. } => Self::ast_uses_changed_function(expr, changed),
10850            ASTNodeType::BinaryOp { left, right, .. } => {
10851                Self::ast_uses_changed_function(left, changed)
10852                    || Self::ast_uses_changed_function(right, changed)
10853            }
10854            ASTNodeType::Array(rows) => rows
10855                .iter()
10856                .flatten()
10857                .any(|node| Self::ast_uses_changed_function(node, changed)),
10858            _ => false,
10859        }
10860    }
10861
10862    fn ast_contains_function(ast: &ASTNode) -> bool {
10863        match &ast.node_type {
10864            ASTNodeType::Function { .. } => true,
10865            ASTNodeType::Call { callee, args } => {
10866                Self::ast_contains_function(callee) || args.iter().any(Self::ast_contains_function)
10867            }
10868            ASTNodeType::UnaryOp { expr, .. } => Self::ast_contains_function(expr),
10869            ASTNodeType::BinaryOp { left, right, .. } => {
10870                Self::ast_contains_function(left) || Self::ast_contains_function(right)
10871            }
10872            ASTNodeType::Array(rows) => rows.iter().flatten().any(Self::ast_contains_function),
10873            ASTNodeType::Literal(_) | ASTNodeType::Omitted | ASTNodeType::Reference { .. } => false,
10874        }
10875    }
10876
10877    fn structural_row_region(sheet_id: SheetId, start_row0: u32) -> Region {
10878        Region::rows_from(sheet_id, start_row0)
10879    }
10880
10881    fn structural_col_region(sheet_id: SheetId, start_col0: u32) -> Region {
10882        Region::cols_from(sheet_id, start_col0)
10883    }
10884
10885    #[cfg(test)]
10886    pub(crate) fn force_non_cycle_schedule_fallback_for_test(&mut self) {
10887        self.force_non_cycle_schedule_fallback_for_test = true;
10888    }
10889
10890    fn materialize_deferred_sheet_before_structural_edit(
10891        &mut self,
10892        sheet: &str,
10893    ) -> Result<(), crate::engine::EditorError> {
10894        if self.staged_formulas.contains_key(sheet) {
10895            self.build_graph_for_sheets([sheet])?;
10896        }
10897        Ok(())
10898    }
10899
10900    fn structural_row_occupancy(
10901        &self,
10902        sheet: &str,
10903        sheet_id: SheetId,
10904    ) -> crate::engine::graph::StructuralOccupancy {
10905        if !self.graph.has_compressed_range_readers() {
10906            return crate::engine::graph::StructuralOccupancy::default();
10907        }
10908        let mut occupancy = self.graph.structural_occupancy(sheet_id);
10909        if let Some(arrow_sheet) = self.arrow_sheets.sheet(sheet) {
10910            occupancy.include_arrow_sheet(arrow_sheet);
10911            occupancy
10912        } else {
10913            // Missing Arrow state cannot prove an apparently empty column empty.
10914            crate::engine::graph::StructuralOccupancy::conservative()
10915        }
10916    }
10917
10918    fn structural_column_occupancy(&self) -> crate::engine::graph::StructuralOccupancy {
10919        // Arrow exposes occupied columns through chunk metadata and overlay maps,
10920        // but has no cheap occupied-row index. Column edits therefore deliberately
10921        // retain conservative cross-axis invalidation instead of scanning cells.
10922        crate::engine::graph::StructuralOccupancy::conservative()
10923    }
10924
10925    /// Insert rows (1-based) and mirror into Arrow store when enabled
10926    pub fn insert_rows(
10927        &mut self,
10928        sheet: &str,
10929        before: u32,
10930        count: u32,
10931    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
10932    {
10933        if count == 0 {
10934            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
10935        }
10936        self.observe_function_semantic_epoch()
10937            .map_err(crate::engine::EditorError::Excel)?;
10938        use crate::engine::graph::editor::vertex_editor::VertexEditor;
10939        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
10940        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10941        let before0 = before.saturating_sub(1);
10942        let affected_region = Self::structural_row_region(sheet_id, before0);
10943        let occupancy = self.structural_row_occupancy(sheet, sheet_id);
10944        let summary = {
10945            let mut editor =
10946                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
10947            editor.insert_rows(sheet_id, before0, count)?
10948        };
10949        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
10950            let before0 = before0 as usize;
10951            asheet.insert_rows(before0, count as usize);
10952        }
10953        self.purge_derived_formats_after_row(sheet_id, before0);
10954        self.mark_moved_formula_vertices_dirty(&summary);
10955        self.clear_computed_overlay_after_row(sheet, before0 as usize);
10956        self.shift_row_visibility_insert(sheet_id, before0, count);
10957        self.record_structural_change(StructuralScope::Region(affected_region));
10958        self.mark_topology_edited();
10959        Ok(summary)
10960    }
10961
10962    /// Delete rows (1-based) and mirror into Arrow store when enabled
10963    pub fn delete_rows(
10964        &mut self,
10965        sheet: &str,
10966        start: u32,
10967        count: u32,
10968    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
10969    {
10970        if count == 0 {
10971            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
10972        }
10973        self.observe_function_semantic_epoch()
10974            .map_err(crate::engine::EditorError::Excel)?;
10975        use crate::engine::graph::editor::vertex_editor::VertexEditor;
10976        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
10977        let sheet_id = self.ensure_known_sheet_id(sheet)?;
10978        let start0 = start.saturating_sub(1);
10979        let affected_region = Self::structural_row_region(sheet_id, start0);
10980        let occupancy = self.structural_row_occupancy(sheet, sheet_id);
10981        let summary = {
10982            let mut editor =
10983                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
10984            editor.delete_rows(sheet_id, start0, count)?
10985        };
10986        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
10987            let start0 = start0 as usize;
10988            asheet.delete_rows(start0, count as usize);
10989        }
10990        self.purge_derived_formats_after_row(sheet_id, start0);
10991        self.mark_moved_formula_vertices_dirty(&summary);
10992        self.clear_computed_overlay_after_row(sheet, start0 as usize);
10993        self.shift_row_visibility_delete(sheet_id, start0, count);
10994        self.record_structural_change(StructuralScope::Region(affected_region));
10995        self.mark_topology_edited();
10996        Ok(summary)
10997    }
10998
10999    /// Insert columns (1-based) and mirror into Arrow store when enabled
11000    pub fn insert_columns(
11001        &mut self,
11002        sheet: &str,
11003        before: u32,
11004        count: u32,
11005    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
11006    {
11007        if count == 0 {
11008            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
11009        }
11010        self.observe_function_semantic_epoch()
11011            .map_err(crate::engine::EditorError::Excel)?;
11012        use crate::engine::graph::editor::vertex_editor::VertexEditor;
11013        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
11014        let sheet_id = self.graph.sheet_id(sheet).ok_or(
11015            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
11016                name: sheet.to_string(),
11017                reason: "Unknown sheet".to_string(),
11018            },
11019        )?;
11020        let before0 = before.saturating_sub(1);
11021        let affected_region = Self::structural_col_region(sheet_id, before0);
11022        let occupancy = self.structural_column_occupancy();
11023        let summary = {
11024            let mut editor =
11025                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
11026            editor.insert_columns(sheet_id, before0, count)?
11027        };
11028        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
11029            let before0 = before0 as usize;
11030            asheet.insert_columns(before0, count as usize);
11031        }
11032        self.purge_derived_formats_after_col(sheet_id, before0);
11033        self.mark_moved_formula_vertices_dirty(&summary);
11034        self.clear_computed_overlay_after_col(sheet, before0 as usize);
11035        self.record_structural_change(StructuralScope::Region(affected_region));
11036        self.mark_topology_edited();
11037        Ok(summary)
11038    }
11039
11040    /// Delete columns (1-based) and mirror into Arrow store when enabled
11041    pub fn delete_columns(
11042        &mut self,
11043        sheet: &str,
11044        start: u32,
11045        count: u32,
11046    ) -> Result<crate::engine::graph::editor::vertex_editor::ShiftSummary, crate::engine::EditorError>
11047    {
11048        if count == 0 {
11049            return Ok(crate::engine::graph::editor::vertex_editor::ShiftSummary::default());
11050        }
11051        self.observe_function_semantic_epoch()
11052            .map_err(crate::engine::EditorError::Excel)?;
11053        use crate::engine::graph::editor::vertex_editor::VertexEditor;
11054        self.materialize_deferred_sheet_before_structural_edit(sheet)?;
11055        let sheet_id = self.graph.sheet_id(sheet).ok_or(
11056            crate::engine::graph::editor::vertex_editor::EditorError::InvalidName {
11057                name: sheet.to_string(),
11058                reason: "Unknown sheet".to_string(),
11059            },
11060        )?;
11061        let start0 = start.saturating_sub(1);
11062        let affected_region = Self::structural_col_region(sheet_id, start0);
11063        let occupancy = self.structural_column_occupancy();
11064        let summary = {
11065            let mut editor =
11066                VertexEditor::new(&mut self.graph).with_structural_occupancy(occupancy);
11067            editor.delete_columns(sheet_id, start0, count)?
11068        };
11069        if let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) {
11070            let start0 = start0 as usize;
11071            asheet.delete_columns(start0, count as usize);
11072        }
11073        self.purge_derived_formats_after_col(sheet_id, start0);
11074        self.mark_moved_formula_vertices_dirty(&summary);
11075        self.clear_computed_overlay_after_col(sheet, start0 as usize);
11076        self.record_structural_change(StructuralScope::Region(affected_region));
11077        self.mark_topology_edited();
11078        Ok(summary)
11079    }
11080    /// Arrow-backed used row bounds across a column span (1-based inclusive cols).
11081    fn arrow_used_row_bounds(
11082        &self,
11083        sheet: &str,
11084        start_col: u32,
11085        end_col: u32,
11086    ) -> Option<(u32, u32)> {
11087        let a = self.sheet_store().sheet(sheet)?;
11088        if a.columns.is_empty() {
11089            return None;
11090        }
11091        let sc0 = start_col.saturating_sub(1) as usize;
11092        let ec0 = end_col.saturating_sub(1) as usize;
11093        let col_hi = a.columns.len().saturating_sub(1);
11094        if sc0 > col_hi {
11095            return None;
11096        }
11097        let ec0 = ec0.min(col_hi);
11098        // Pass-scoped cache with snapshot guard
11099        let snap = self.data_snapshot_id();
11100        let mut min_r0: Option<usize> = None;
11101        for ci in sc0..=ec0 {
11102            let sheet_id = self.graph.sheet_id(sheet)?;
11103            if let Some((Some(mv), _)) = self.row_bounds_cache.read().ok().and_then(|g| {
11104                g.as_ref()
11105                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
11106            }) {
11107                let mv = mv as usize;
11108                min_r0 = Some(min_r0.map(|m| m.min(mv)).unwrap_or(mv));
11109                continue;
11110            }
11111            // Compute and store
11112            let (min_c, max_c) = Self::scan_column_used_bounds(a, ci);
11113            if let Ok(mut g) = self.row_bounds_cache.write() {
11114                g.get_or_insert_with(|| RowBoundsCache::new(snap))
11115                    .put_row_bounds(sheet_id, ci, snap, (min_c, max_c));
11116            }
11117            if let Some(m) = min_c {
11118                min_r0 = Some(min_r0.map(|mm| mm.min(m as usize)).unwrap_or(m as usize));
11119            }
11120        }
11121        min_r0?;
11122        let mut max_r0: Option<usize> = None;
11123        for ci in sc0..=ec0 {
11124            let sheet_id = self.graph.sheet_id(sheet)?;
11125            if let Some((_, Some(mv))) = self.row_bounds_cache.read().ok().and_then(|g| {
11126                g.as_ref()
11127                    .and_then(|c| c.get_row_bounds(sheet_id, ci, snap))
11128            }) {
11129                let mv = mv as usize;
11130                max_r0 = Some(max_r0.map(|m| m.max(mv)).unwrap_or(mv));
11131                continue;
11132            }
11133            let (_min_c, max_c) = Self::scan_column_used_bounds(a, ci);
11134            if let Ok(mut g) = self.row_bounds_cache.write() {
11135                g.get_or_insert_with(|| RowBoundsCache::new(snap))
11136                    .put_row_bounds(sheet_id, ci, snap, (_min_c, max_c));
11137            }
11138            if let Some(m) = max_c {
11139                max_r0 = Some(max_r0.map(|mm| mm.max(m as usize)).unwrap_or(m as usize));
11140            }
11141        }
11142        match (min_r0, max_r0) {
11143            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
11144            _ => None,
11145        }
11146    }
11147
11148    fn scan_column_used_bounds(
11149        a: &crate::arrow_store::ArrowSheet,
11150        ci: usize,
11151    ) -> (Option<u32>, Option<u32>) {
11152        let col = &a.columns[ci];
11153
11154        // Min: scan dense chunks first, then sparse chunks in ascending index order.
11155        let mut min_r0: Option<u32> = None;
11156        for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
11157            let tags = chunk.type_tag.values();
11158            for (off, &t) in tags.iter().enumerate() {
11159                let overlay_non_empty = chunk
11160                    .overlay
11161                    .get(off)
11162                    .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11163                    .unwrap_or(false)
11164                    || chunk
11165                        .computed_overlay
11166                        .get(off)
11167                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11168                        .unwrap_or(false);
11169                if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
11170                    let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
11171                        break;
11172                    };
11173                    let row0 = chunk_start + off;
11174                    min_r0 = Some(row0 as u32);
11175                    break;
11176                }
11177            }
11178            if min_r0.is_some() {
11179                break;
11180            }
11181        }
11182        if min_r0.is_none() && !col.sparse_chunks.is_empty() {
11183            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
11184            sparse_idxs.sort_unstable();
11185            for chunk_idx in sparse_idxs {
11186                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
11187                    continue;
11188                };
11189                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
11190                    continue;
11191                };
11192                let tags = chunk.type_tag.values();
11193                for (off, &t) in tags.iter().enumerate() {
11194                    let overlay_non_empty = chunk
11195                        .overlay
11196                        .get(off)
11197                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11198                        .unwrap_or(false)
11199                        || chunk
11200                            .computed_overlay
11201                            .get(off)
11202                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11203                            .unwrap_or(false);
11204                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
11205                        let row0 = chunk_start + off;
11206                        min_r0 = Some(row0 as u32);
11207                        break;
11208                    }
11209                }
11210                if min_r0.is_some() {
11211                    break;
11212                }
11213            }
11214        }
11215
11216        // Max: scan sparse chunks in descending index order, then dense chunks in reverse.
11217        let mut max_r0: Option<u32> = None;
11218        if !col.sparse_chunks.is_empty() {
11219            let mut sparse_idxs: Vec<usize> = col.sparse_chunks.keys().copied().collect();
11220            sparse_idxs.sort_unstable_by(|a, b| b.cmp(a));
11221            for chunk_idx in sparse_idxs {
11222                let Some(chunk) = col.sparse_chunks.get(&chunk_idx) else {
11223                    continue;
11224                };
11225                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
11226                    continue;
11227                };
11228                let tags = chunk.type_tag.values();
11229                for (rev_idx, &t) in tags.iter().enumerate().rev() {
11230                    let overlay_non_empty = chunk
11231                        .overlay
11232                        .get(rev_idx)
11233                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11234                        .unwrap_or(false)
11235                        || chunk
11236                            .computed_overlay
11237                            .get(rev_idx)
11238                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11239                            .unwrap_or(false);
11240                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
11241                        let row0 = chunk_start + rev_idx;
11242                        max_r0 = Some(row0 as u32);
11243                        break;
11244                    }
11245                }
11246                if max_r0.is_some() {
11247                    break;
11248                }
11249            }
11250        }
11251        if max_r0.is_none() {
11252            for (chunk_idx, chunk) in col.chunks.iter().enumerate().rev() {
11253                let tags = chunk.type_tag.values();
11254                for (rev_idx, &t) in tags.iter().enumerate().rev() {
11255                    let overlay_non_empty = chunk
11256                        .overlay
11257                        .get(rev_idx)
11258                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11259                        .unwrap_or(false)
11260                        || chunk
11261                            .computed_overlay
11262                            .get(rev_idx)
11263                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11264                            .unwrap_or(false);
11265                    if overlay_non_empty || t != crate::arrow_store::TypeTag::Empty as u8 {
11266                        let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
11267                            break;
11268                        };
11269                        let row0 = chunk_start + rev_idx;
11270                        max_r0 = Some(row0 as u32);
11271                        break;
11272                    }
11273                }
11274                if max_r0.is_some() {
11275                    break;
11276                }
11277            }
11278        }
11279
11280        (min_r0, max_r0)
11281    }
11282
11283    /// Arrow-backed used column bounds across a row span (1-based inclusive rows).
11284    fn arrow_used_col_bounds(
11285        &self,
11286        sheet: &str,
11287        start_row: u32,
11288        end_row: u32,
11289    ) -> Option<(u32, u32)> {
11290        let a = self.sheet_store().sheet(sheet)?;
11291        if a.columns.is_empty() {
11292            return None;
11293        }
11294        let sr0 = start_row.saturating_sub(1) as usize;
11295        let er0 = end_row.saturating_sub(1) as usize;
11296        if sr0 > er0 {
11297            return None;
11298        }
11299        // Map start/end rows into chunk ranges
11300        // We will scan each column for any non-empty within [sr0..=er0]
11301        let mut min_c0: Option<usize> = None;
11302        let mut max_c0: Option<usize> = None;
11303        // Precompute chunk bounds for row range
11304        for (ci, col) in a.columns.iter().enumerate() {
11305            let mut any_in_range = false;
11306
11307            let scan_chunk = |chunk_idx: usize, chunk: &crate::arrow_store::ColumnChunk| -> bool {
11308                let Some(&chunk_start) = a.chunk_starts.get(chunk_idx) else {
11309                    return false;
11310                };
11311                let chunk_len = chunk.type_tag.len();
11312                if chunk_len == 0 {
11313                    return false;
11314                }
11315                let chunk_end = chunk_start + chunk_len.saturating_sub(1);
11316                // check intersection
11317                if sr0 > chunk_end || er0 < chunk_start {
11318                    return false;
11319                }
11320                let start_off = sr0.max(chunk_start) - chunk_start;
11321                let end_off = er0.min(chunk_end) - chunk_start;
11322                let tags = chunk.type_tag.values();
11323                for off in start_off..=end_off {
11324                    let overlay_non_empty = chunk
11325                        .overlay
11326                        .get(off)
11327                        .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11328                        .unwrap_or(false)
11329                        || chunk
11330                            .computed_overlay
11331                            .get(off)
11332                            .map(|ov| !matches!(ov, crate::arrow_store::OverlayValue::Empty))
11333                            .unwrap_or(false);
11334                    if overlay_non_empty || tags[off] != crate::arrow_store::TypeTag::Empty as u8 {
11335                        return true;
11336                    }
11337                }
11338                false
11339            };
11340
11341            for (chunk_idx, chunk) in col.chunks.iter().enumerate() {
11342                if scan_chunk(chunk_idx, chunk) {
11343                    any_in_range = true;
11344                    break;
11345                }
11346            }
11347
11348            if !any_in_range && !col.sparse_chunks.is_empty() {
11349                for (&chunk_idx, chunk) in col.sparse_chunks.iter() {
11350                    if scan_chunk(chunk_idx, chunk) {
11351                        any_in_range = true;
11352                        break;
11353                    }
11354                }
11355            }
11356
11357            if any_in_range {
11358                min_c0 = Some(min_c0.map(|m| m.min(ci)).unwrap_or(ci));
11359                max_c0 = Some(max_c0.map(|m| m.max(ci)).unwrap_or(ci));
11360            }
11361        }
11362        match (min_c0, max_c0) {
11363            (Some(a0), Some(b0)) => Some(((a0 as u32) + 1, (b0 as u32) + 1)),
11364            _ => None,
11365        }
11366    }
11367
11368    fn formula_row_bounds_for_columns(
11369        &self,
11370        sheet: &str,
11371        start_col: u32,
11372        end_col: u32,
11373    ) -> Option<(u32, u32)> {
11374        let sheet_id = self.graph.sheet_id(sheet)?;
11375        let sc0 = start_col.saturating_sub(1);
11376        let ec0 = end_col.saturating_sub(1);
11377        let mut min_r0: Option<u32> = None;
11378        let mut max_r0: Option<u32> = None;
11379
11380        if self.graph.sheet_index(sheet_id).is_some() {
11381            for vid in self.graph.vertices_in_cols(sheet_id, sc0, ec0) {
11382                if !matches!(
11383                    self.graph.get_vertex_kind(vid),
11384                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11385                ) {
11386                    continue;
11387                }
11388                let Some(row0) = self.graph.vertex_grid_addr(vid).map(|addr| addr.row()) else {
11389                    continue;
11390                };
11391                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
11392                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
11393            }
11394        } else {
11395            for (vid, coord) in self.graph.grid_vertices_in_sheet(sheet_id) {
11396                if !matches!(
11397                    self.graph.get_vertex_kind(vid),
11398                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11399                ) {
11400                    continue;
11401                }
11402                let col0 = coord.col();
11403                if col0 < sc0 || col0 > ec0 {
11404                    continue;
11405                }
11406                let row0 = coord.row();
11407                min_r0 = Some(min_r0.map(|m| m.min(row0)).unwrap_or(row0));
11408                max_r0 = Some(max_r0.map(|m| m.max(row0)).unwrap_or(row0));
11409            }
11410        }
11411
11412        match (min_r0, max_r0) {
11413            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
11414            _ => None,
11415        }
11416    }
11417
11418    fn formula_col_bounds_for_rows(
11419        &self,
11420        sheet: &str,
11421        start_row: u32,
11422        end_row: u32,
11423    ) -> Option<(u32, u32)> {
11424        let sheet_id = self.graph.sheet_id(sheet)?;
11425        let sr0 = start_row.saturating_sub(1);
11426        let er0 = end_row.saturating_sub(1);
11427        let mut min_c0: Option<u32> = None;
11428        let mut max_c0: Option<u32> = None;
11429
11430        if self.graph.sheet_index(sheet_id).is_some() {
11431            for vid in self.graph.vertices_in_rows(sheet_id, sr0, er0) {
11432                if !matches!(
11433                    self.graph.get_vertex_kind(vid),
11434                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11435                ) {
11436                    continue;
11437                }
11438                let Some(col0) = self.graph.vertex_grid_addr(vid).map(|addr| addr.col()) else {
11439                    continue;
11440                };
11441                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
11442                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
11443            }
11444        } else {
11445            for (vid, coord) in self.graph.grid_vertices_in_sheet(sheet_id) {
11446                if !matches!(
11447                    self.graph.get_vertex_kind(vid),
11448                    VertexKind::FormulaScalar | VertexKind::FormulaArray
11449                ) {
11450                    continue;
11451                }
11452                let row0 = coord.row();
11453                if row0 < sr0 || row0 > er0 {
11454                    continue;
11455                }
11456                let col0 = coord.col();
11457                min_c0 = Some(min_c0.map(|m| m.min(col0)).unwrap_or(col0));
11458                max_c0 = Some(max_c0.map(|m| m.max(col0)).unwrap_or(col0));
11459            }
11460        }
11461
11462        match (min_c0, max_c0) {
11463            (Some(a0), Some(b0)) => Some((a0 + 1, b0 + 1)),
11464            _ => None,
11465        }
11466    }
11467
11468    fn union_used_bounds(
11469        first: Option<(u32, u32)>,
11470        second: Option<(u32, u32)>,
11471    ) -> Option<(u32, u32)> {
11472        match (first, second) {
11473            (Some((a0, b0)), Some((a1, b1))) => Some((a0.min(a1), b0.max(b1))),
11474            (Some(bounds), None) | (None, Some(bounds)) => Some(bounds),
11475            (None, None) => None,
11476        }
11477    }
11478
11479    /// Mirror a single cell value into the Arrow overlay if enabled.
11480    /// Handles capacity growth, per-chunk overlay set, and heuristic compaction.
11481    fn mirror_value_to_overlay(&mut self, sheet: &str, row: u32, col: u32, value: &LiteralValue) {
11482        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11483            return;
11484        }
11485        if self.arrow_sheets.sheet(sheet).is_none() {
11486            self.arrow_sheets
11487                .sheets
11488                .push(crate::arrow_store::ArrowSheet {
11489                    name: std::sync::Arc::<str>::from(sheet),
11490                    date_system: self.config.date_system,
11491                    columns: Vec::new(),
11492                    nrows: 0,
11493                    chunk_starts: Vec::new(),
11494                    chunk_rows: 32 * 1024,
11495                });
11496        }
11497
11498        let row0 = row.saturating_sub(1) as usize;
11499        let col0 = col.saturating_sub(1) as usize;
11500
11501        let asheet = self
11502            .arrow_sheets
11503            .sheet_mut(sheet)
11504            .expect("ArrowSheet must exist");
11505
11506        let cur_cols = asheet.columns.len();
11507        if col0 >= cur_cols {
11508            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11509        }
11510
11511        if row0 >= asheet.nrows as usize {
11512            if asheet.columns.is_empty() {
11513                asheet.insert_columns(0, 1);
11514            }
11515            asheet.ensure_row_capacity(row0 + 1);
11516        }
11517        if let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) {
11518            let ov =
11519                crate::arrow_store::OverlayValue::from_literal_value(value, asheet.date_system);
11520            let computed_delta = if let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) {
11521                let _ = ch.overlay.set(in_off, ov);
11522                let format = match value {
11523                    LiteralValue::Date(_) => Some(crate::format::FormatId::DATE),
11524                    LiteralValue::DateTime(_) => Some(crate::format::FormatId::DATETIME),
11525                    LiteralValue::Time(_) => Some(crate::format::FormatId::TIME),
11526                    LiteralValue::Duration(_) => Some(crate::format::FormatId::DURATION),
11527                    _ => None,
11528                };
11529                ch.overlay.set_format(in_off, format);
11530                // A user edit must invalidate any computed (formula/spill) overlay entry at
11531                // this cell. Otherwise, if the delta overlay later compacts into the base lanes
11532                // (clearing `overlay`), a stale `computed_overlay=Empty` could incorrectly mask
11533                // the edited base value under the read cascade.
11534                ch.computed_overlay.remove(in_off)
11535            } else {
11536                return;
11537            };
11538            // Heuristic compaction: > len/50 or > 1024
11539            let abs_threshold = 1024usize;
11540            let frac_den = 50usize;
11541            let freed = asheet.maybe_compact_chunk(col0, ch_idx, abs_threshold, frac_den);
11542            if freed > 0 {
11543                self.overlay_compactions = self.overlay_compactions.saturating_add(1);
11544            }
11545            self.adjust_computed_overlay_bytes(computed_delta);
11546        }
11547    }
11548
11549    /// Remove a delta-overlay entry for a single cell (if present).
11550    ///
11551    /// This is used when transitioning a cell to a formula so that any previous user-edit overlay
11552    /// does not continue to mask computed overlay outputs.
11553    fn clear_delta_overlay_cell(&mut self, sheet: &str, row: u32, col: u32) {
11554        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11555            return;
11556        }
11557        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11558            return;
11559        };
11560        let row0 = row.saturating_sub(1) as usize;
11561        let col0 = col.saturating_sub(1) as usize;
11562        if row0 >= asheet.nrows as usize {
11563            return;
11564        }
11565        if col0 >= asheet.columns.len() {
11566            return;
11567        }
11568        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11569            return;
11570        };
11571        if let Some(ch) = asheet.columns[col0].chunk_mut(ch_idx) {
11572            let _ = ch.overlay.remove(in_off);
11573        }
11574    }
11575
11576    fn clear_computed_overlay_after_row(&mut self, sheet: &str, start_row0: usize) {
11577        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
11578            return;
11579        }
11580
11581        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11582            return;
11583        };
11584        if start_row0 >= asheet.nrows as usize {
11585            return;
11586        }
11587
11588        let starts = asheet.chunk_starts.clone();
11589        let nrows = asheet.nrows as usize;
11590        let mut delta = 0isize;
11591        for col in &mut asheet.columns {
11592            for (chunk_idx, ch) in col.chunks.iter_mut().enumerate() {
11593                let Some(&chunk_start) = starts.get(chunk_idx) else {
11594                    continue;
11595                };
11596                let chunk_end = starts
11597                    .get(chunk_idx + 1)
11598                    .copied()
11599                    .unwrap_or(nrows)
11600                    .min(chunk_start.saturating_add(ch.len()));
11601                if chunk_end <= start_row0 {
11602                    continue;
11603                }
11604                if chunk_start >= start_row0 {
11605                    delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11606                } else {
11607                    let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
11608                    delta = delta
11609                        .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
11610                }
11611            }
11612
11613            for (chunk_idx, ch) in &mut col.sparse_chunks {
11614                let Some(&chunk_start) = starts.get(*chunk_idx) else {
11615                    continue;
11616                };
11617                let chunk_end = starts
11618                    .get(*chunk_idx + 1)
11619                    .copied()
11620                    .unwrap_or(nrows)
11621                    .min(chunk_start.saturating_add(ch.len()));
11622                if chunk_end <= start_row0 {
11623                    continue;
11624                }
11625                if chunk_start >= start_row0 {
11626                    delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11627                } else {
11628                    let start_in_chunk = start_row0.saturating_sub(chunk_start).min(ch.len());
11629                    delta = delta
11630                        .saturating_add(ch.computed_overlay.remove_range(start_in_chunk..ch.len()));
11631                }
11632            }
11633        }
11634        self.adjust_computed_overlay_bytes(delta);
11635    }
11636
11637    fn clear_computed_overlay_after_col(&mut self, sheet: &str, start_col0: usize) {
11638        if !(self.config.arrow_storage_enabled && self.config.write_formula_overlay_enabled) {
11639            return;
11640        }
11641
11642        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
11643            return;
11644        };
11645        if start_col0 >= asheet.columns.len() {
11646            return;
11647        }
11648
11649        let mut delta = 0isize;
11650        for col in asheet.columns.iter_mut().skip(start_col0) {
11651            for ch in &mut col.chunks {
11652                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11653            }
11654            for ch in col.sparse_chunks.values_mut() {
11655                delta = delta.saturating_sub(ch.computed_overlay.clear() as isize);
11656            }
11657        }
11658        self.adjust_computed_overlay_bytes(delta);
11659    }
11660
11661    #[inline]
11662    fn literal_to_overlay_value(
11663        value: &LiteralValue,
11664        date_system: crate::engine::DateSystem,
11665    ) -> crate::arrow_store::OverlayValue {
11666        crate::arrow_store::OverlayValue::from_literal_value(value, date_system)
11667    }
11668
11669    fn arrow_sheet_date_system(&self, sheet: &str) -> crate::engine::DateSystem {
11670        self.arrow_sheets
11671            .sheet(sheet)
11672            .map(|sheet| sheet.date_system)
11673            .unwrap_or(self.config.date_system)
11674    }
11675
11676    /// Read a single cell's delta overlay entry (if present), preserving the distinction between
11677    /// absent and explicit `Empty`.
11678    fn read_delta_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
11679        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11680            return None;
11681        }
11682        let asheet = self.arrow_sheets.sheet(sheet)?;
11683        let row0 = row.saturating_sub(1) as usize;
11684        let col0 = col.saturating_sub(1) as usize;
11685        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
11686            return None;
11687        }
11688        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
11689        let ch = asheet.columns[col0].chunk(ch_idx)?;
11690        ch.overlay
11691            .get_scalar(in_off)
11692            .map(|ov| ov.to_literal_for(asheet.date_system))
11693    }
11694
11695    /// Read a single cell's computed overlay entry (if present), preserving the distinction
11696    /// between absent and explicit `Empty`.
11697    fn read_computed_overlay_cell(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
11698        if !(self.config.arrow_storage_enabled
11699            && self.config.delta_overlay_enabled
11700            && self.config.write_formula_overlay_enabled)
11701        {
11702            return None;
11703        }
11704        let asheet = self.arrow_sheets.sheet(sheet)?;
11705        let row0 = row.saturating_sub(1) as usize;
11706        let col0 = col.saturating_sub(1) as usize;
11707        if row0 >= asheet.nrows as usize || col0 >= asheet.columns.len() {
11708            return None;
11709        }
11710        let (ch_idx, in_off) = asheet.chunk_of_row(row0)?;
11711        let ch = asheet.columns[col0].chunk(ch_idx)?;
11712        ch.computed_overlay
11713            .get_scalar(in_off)
11714            .map(|ov| ov.to_literal_for(asheet.date_system))
11715    }
11716
11717    fn set_delta_overlay_cell_raw(
11718        &mut self,
11719        sheet: &str,
11720        row: u32,
11721        col: u32,
11722        value: Option<LiteralValue>,
11723    ) {
11724        if !(self.config.arrow_storage_enabled && self.config.delta_overlay_enabled) {
11725            return;
11726        }
11727
11728        self.ensure_arrow_sheet(sheet);
11729        let date_system = self.arrow_sheet_date_system(sheet);
11730        let ov_opt = value
11731            .as_ref()
11732            .map(|value| Self::literal_to_overlay_value(value, date_system));
11733        let row0 = row.saturating_sub(1) as usize;
11734        let col0 = col.saturating_sub(1) as usize;
11735        let asheet = self
11736            .arrow_sheets
11737            .sheet_mut(sheet)
11738            .expect("ArrowSheet must exist");
11739
11740        let cur_cols = asheet.columns.len();
11741        if col0 >= cur_cols {
11742            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11743        }
11744        if row0 >= asheet.nrows as usize {
11745            if asheet.columns.is_empty() {
11746                asheet.insert_columns(0, 1);
11747            }
11748            asheet.ensure_row_capacity(row0 + 1);
11749        }
11750
11751        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11752            return;
11753        };
11754        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
11755            return;
11756        };
11757
11758        if let Some(ov) = ov_opt {
11759            let _ = ch.overlay.set(in_off, ov);
11760        } else {
11761            let _ = ch.overlay.remove(in_off);
11762        }
11763    }
11764
11765    fn set_computed_overlay_cell_raw(
11766        &mut self,
11767        sheet: &str,
11768        row: u32,
11769        col: u32,
11770        value: Option<LiteralValue>,
11771    ) {
11772        if !(self.config.arrow_storage_enabled
11773            && self.config.delta_overlay_enabled
11774            && self.config.write_formula_overlay_enabled)
11775        {
11776            return;
11777        }
11778
11779        self.ensure_arrow_sheet(sheet);
11780        let date_system = self.arrow_sheet_date_system(sheet);
11781        let ov_opt = value
11782            .as_ref()
11783            .map(|value| Self::literal_to_overlay_value(value, date_system));
11784        let row0 = row.saturating_sub(1) as usize;
11785        let col0 = col.saturating_sub(1) as usize;
11786        let asheet = self
11787            .arrow_sheets
11788            .sheet_mut(sheet)
11789            .expect("ArrowSheet must exist");
11790
11791        let cur_cols = asheet.columns.len();
11792        if col0 >= cur_cols {
11793            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
11794        }
11795        if row0 >= asheet.nrows as usize {
11796            if asheet.columns.is_empty() {
11797                asheet.insert_columns(0, 1);
11798            }
11799            asheet.ensure_row_capacity(row0 + 1);
11800        }
11801
11802        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
11803            return;
11804        };
11805        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
11806            return;
11807        };
11808
11809        let delta = if let Some(ov) = ov_opt {
11810            ch.computed_overlay.set(in_off, ov)
11811        } else {
11812            ch.computed_overlay.remove(in_off)
11813        };
11814        self.adjust_computed_overlay_bytes(delta);
11815    }
11816
11817    fn apply_arrow_undo_batch(&mut self, batch: &crate::engine::ArrowUndoBatch, undo: bool) {
11818        use crate::engine::ArrowOp;
11819
11820        let iter: Box<dyn Iterator<Item = &ArrowOp>> = if undo {
11821            Box::new(batch.ops.iter().rev())
11822        } else {
11823            Box::new(batch.ops.iter())
11824        };
11825
11826        for op in iter {
11827            match op {
11828                ArrowOp::SetDeltaCell {
11829                    sheet_id,
11830                    row0,
11831                    col0,
11832                    old,
11833                    new,
11834                } => {
11835                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11836                    let v = if undo { old.clone() } else { new.clone() };
11837                    self.set_delta_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
11838                }
11839                ArrowOp::SetComputedCell {
11840                    sheet_id,
11841                    row0,
11842                    col0,
11843                    old,
11844                    new,
11845                } => {
11846                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11847                    let v = if undo { old.clone() } else { new.clone() };
11848                    self.set_computed_overlay_cell_raw(&sheet, row0 + 1, col0 + 1, v);
11849                }
11850                ArrowOp::RestoreComputedRect {
11851                    sheet_id,
11852                    sr0,
11853                    sc0,
11854                    er0,
11855                    ec0,
11856                    old,
11857                    new,
11858                } => {
11859                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11860                    let vals = if undo { old } else { new };
11861                    let height = (*er0).saturating_sub(*sr0) as usize + 1;
11862                    let width = (*ec0).saturating_sub(*sc0) as usize + 1;
11863                    for r in 0..height {
11864                        for c in 0..width {
11865                            let v = vals
11866                                .get(r)
11867                                .and_then(|row| row.get(c))
11868                                .cloned()
11869                                .unwrap_or(LiteralValue::Empty);
11870                            self.set_computed_overlay_cell_raw(
11871                                &sheet,
11872                                *sr0 + 1 + r as u32,
11873                                *sc0 + 1 + c as u32,
11874                                Some(v),
11875                            );
11876                        }
11877                    }
11878                }
11879                ArrowOp::InsertRows {
11880                    sheet_id,
11881                    before0,
11882                    count,
11883                } => {
11884                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11885                    self.ensure_arrow_sheet(&sheet);
11886                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
11887                        if undo {
11888                            asheet.delete_rows(*before0 as usize, *count as usize);
11889                        } else {
11890                            asheet.insert_rows(*before0 as usize, *count as usize);
11891                        }
11892                    }
11893                    self.purge_derived_formats_after_row(*sheet_id, *before0);
11894                }
11895                ArrowOp::InsertCols {
11896                    sheet_id,
11897                    before0,
11898                    count,
11899                } => {
11900                    let sheet = self.graph.sheet_name(*sheet_id).to_string();
11901                    self.ensure_arrow_sheet(&sheet);
11902                    if let Some(asheet) = self.arrow_sheets.sheet_mut(&sheet) {
11903                        if undo {
11904                            asheet.delete_columns(*before0 as usize, *count as usize);
11905                        } else {
11906                            asheet.insert_columns(*before0 as usize, *count as usize);
11907                        }
11908                    }
11909                    self.purge_derived_formats_after_col(*sheet_id, *before0);
11910                }
11911            }
11912        }
11913    }
11914
11915    fn record_spill_ops_into_arrow_undo(
11916        &mut self,
11917        undo: &mut crate::engine::ArrowUndoBatch,
11918        events: &[crate::engine::ChangeEvent],
11919    ) {
11920        use crate::engine::ChangeEvent;
11921        use formualizer_common::LiteralValue;
11922
11923        #[allow(clippy::type_complexity)]
11924        let rect_from_snapshot =
11925            |snap: &crate::engine::graph::editor::change_log::SpillSnapshot|
11926             -> Option<(SheetId, u32, u32, u32, u32, Vec<Vec<LiteralValue>>)> {
11927                if snap.target_cells.is_empty() {
11928                    return None;
11929                }
11930                let sheet_id = snap.target_cells[0].sheet_id;
11931                let sr0 = snap.target_cells[0].coord.row();
11932                let sc0 = snap.target_cells[0].coord.col();
11933                if snap.values.is_empty() || snap.values[0].is_empty() {
11934                    return None;
11935                }
11936                let h = snap.values.len() as u32;
11937                let w = snap.values[0].len() as u32;
11938                let er0 = sr0.saturating_add(h.saturating_sub(1));
11939                let ec0 = sc0.saturating_add(w.saturating_sub(1));
11940                Some((sheet_id, sr0, sc0, er0, ec0, snap.values.clone()))
11941            };
11942
11943        for ev in events {
11944            match ev {
11945                ChangeEvent::SpillCommitted { old, new, .. } => {
11946                    if let Some((sid, sr0, sc0, er0, ec0, new_vals)) = rect_from_snapshot(new) {
11947                        let old_vals = if let Some(old_snap) = old {
11948                            rect_from_snapshot(old_snap)
11949                                .map(|(_, _, _, _, _, v)| v)
11950                                .unwrap_or_else(|| {
11951                                    vec![
11952                                        vec![LiteralValue::Empty; new_vals[0].len()];
11953                                        new_vals.len()
11954                                    ]
11955                                })
11956                        } else {
11957                            vec![vec![LiteralValue::Empty; new_vals[0].len()]; new_vals.len()]
11958                        };
11959                        undo.record_restore_computed_rect(
11960                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
11961                        );
11962                    }
11963                }
11964                ChangeEvent::SpillCleared { old, .. } => {
11965                    if let Some((sid, sr0, sc0, er0, ec0, old_vals)) = rect_from_snapshot(old) {
11966                        let new_vals =
11967                            vec![vec![LiteralValue::Empty; old_vals[0].len()]; old_vals.len()];
11968                        undo.record_restore_computed_rect(
11969                            sid, sr0, sc0, er0, ec0, old_vals, new_vals,
11970                        );
11971                    }
11972                }
11973                _ => {}
11974            }
11975        }
11976    }
11977
11978    /// Mirror a value into the computed overlay (formula/spill outputs).
11979    ///
11980    /// This path is subject to `EvalConfig.max_overlay_memory_bytes`.
11981    /// If the cap is exceeded, computed overlays are compacted into base lanes.
11982    fn mirror_value_to_computed_overlay(
11983        &mut self,
11984        sheet: &str,
11985        row: u32,
11986        col: u32,
11987        value: &LiteralValue,
11988    ) {
11989        if !(self.config.arrow_storage_enabled
11990            && self.config.delta_overlay_enabled
11991            && self.config.write_formula_overlay_enabled)
11992        {
11993            return;
11994        }
11995        if self.computed_overlay_mirroring_disabled {
11996            return;
11997        }
11998
11999        let date_system = self.arrow_sheet_date_system(sheet);
12000        let ov = Self::literal_to_overlay_value(value, date_system);
12001        self.write_computed_overlay_value_0based(
12002            sheet,
12003            row.saturating_sub(1),
12004            col.saturating_sub(1),
12005            ov,
12006        );
12007    }
12008
12009    fn record_derived_format(&self, vertex_id: VertexId, format: Option<crate::format::FormatId>) {
12010        if let Some(cell) = self.graph.get_cell_ref(vertex_id) {
12011            self.record_derived_format_at(cell, format);
12012        }
12013    }
12014
12015    fn record_derived_format_at(&self, cell: CellRef, format: Option<crate::format::FormatId>) {
12016        #[cfg(test)]
12017        self.derived_format_operations_for_test
12018            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12019        let format = format.filter(|id| *id != crate::format::FormatId::GENERAL);
12020        self.derived_formats.set(cell, format);
12021    }
12022
12023    fn clear_cell_format_state(&mut self, sheet: &str, cell: CellRef) {
12024        self.derived_formats.set(cell, None);
12025        if let Some(arrow) = self.arrow_sheets.sheet_mut(sheet) {
12026            arrow.clear_format(cell.coord.row() as usize, cell.coord.col() as usize);
12027        }
12028    }
12029
12030    fn clear_logged_cell_format_states(&mut self, events: &[ChangeEvent]) {
12031        let cells = events
12032            .iter()
12033            .filter_map(|event| match event {
12034                ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
12035                    Some(*addr)
12036                }
12037                _ => None,
12038            })
12039            .collect::<FxHashSet<_>>();
12040        for cell in cells {
12041            let sheet = self.graph.sheet_name(cell.sheet_id).to_string();
12042            self.clear_cell_format_state(&sheet, cell);
12043        }
12044    }
12045
12046    fn purge_derived_formats_after_row(&mut self, sheet_id: SheetId, start0: u32) {
12047        self.derived_formats
12048            .retain(|cell| cell.sheet_id != sheet_id || cell.coord.row() < start0);
12049    }
12050
12051    fn purge_derived_formats_after_col(&mut self, sheet_id: SheetId, start0: u32) {
12052        self.derived_formats
12053            .retain(|cell| cell.sheet_id != sheet_id || cell.coord.col() < start0);
12054    }
12055
12056    fn purge_derived_formats_for_sheet(&mut self, sheet_id: SheetId) {
12057        self.derived_formats
12058            .retain(|cell| cell.sheet_id != sheet_id);
12059    }
12060
12061    #[cfg(test)]
12062    pub(crate) fn debug_computed_overlay_format_0based(
12063        &self,
12064        sheet: &str,
12065        row0: u32,
12066        col0: u32,
12067    ) -> Option<crate::format::FormatId> {
12068        let sheet = self.arrow_sheets.sheet(sheet)?;
12069        let (chunk_idx, row_in_chunk) = sheet.chunk_of_row(row0 as usize)?;
12070        sheet
12071            .columns
12072            .get(col0 as usize)?
12073            .chunk(chunk_idx)?
12074            .computed_overlay
12075            .get_format(row_in_chunk)
12076    }
12077
12078    #[cfg(test)]
12079    pub(crate) fn debug_computed_overlay_chunk_has_formats_0based(
12080        &self,
12081        sheet: &str,
12082        row0: u32,
12083        col0: u32,
12084    ) -> bool {
12085        let Some(sheet) = self.arrow_sheets.sheet(sheet) else {
12086            return false;
12087        };
12088        let Some((chunk_idx, _)) = sheet.chunk_of_row(row0 as usize) else {
12089            return false;
12090        };
12091        sheet
12092            .columns
12093            .get(col0 as usize)
12094            .and_then(|column| column.chunk(chunk_idx))
12095            .is_some_and(|chunk| chunk.computed_overlay.has_formats())
12096    }
12097
12098    #[cfg(test)]
12099    pub(crate) fn debug_clear_derived_format_0based(&mut self, sheet: &str, row0: u32, col0: u32) {
12100        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
12101            self.derived_formats
12102                .set(CellRef::new_absolute(sheet_id, row0, col0), None);
12103        }
12104    }
12105
12106    #[cfg(test)]
12107    pub(crate) fn debug_record_derived_format_0based(
12108        &self,
12109        sheet: &str,
12110        row0: u32,
12111        col0: u32,
12112        format: Option<crate::format::FormatId>,
12113    ) {
12114        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
12115            self.record_derived_format_at(CellRef::new_absolute(sheet_id, row0, col0), format);
12116        }
12117    }
12118
12119    #[cfg(test)]
12120    pub(crate) fn debug_derived_format_0based(
12121        &self,
12122        sheet: &str,
12123        row0: u32,
12124        col0: u32,
12125    ) -> Option<crate::format::FormatId> {
12126        let sheet_id = self.graph.sheet_id(sheet)?;
12127        self.derived_formats
12128            .get(&CellRef::new_absolute(sheet_id, row0, col0))
12129    }
12130
12131    #[cfg(test)]
12132    pub(crate) fn debug_reset_format_write_operation_counts(&mut self) {
12133        self.derived_format_operations_for_test
12134            .store(0, std::sync::atomic::Ordering::Relaxed);
12135        self.computed_overlay_set_explicit_entry_operations_for_test = 0;
12136        self.computed_overlay_stale_clear_range_effects_for_test = 0;
12137        self.computed_overlay_stale_clear_offset_attempts_for_test = 0;
12138        self.computed_format_vector_allocations_for_test
12139            .store(0, std::sync::atomic::Ordering::Relaxed);
12140    }
12141
12142    #[cfg(test)]
12143    pub(crate) fn debug_format_write_operation_counts(&self) -> (u64, u64, u64, u64, u64) {
12144        (
12145            self.derived_format_operations_for_test
12146                .load(std::sync::atomic::Ordering::Relaxed),
12147            self.computed_overlay_set_explicit_entry_operations_for_test,
12148            self.computed_format_vector_allocations_for_test
12149                .load(std::sync::atomic::Ordering::Relaxed),
12150            self.computed_overlay_stale_clear_range_effects_for_test,
12151            self.computed_overlay_stale_clear_offset_attempts_for_test,
12152        )
12153    }
12154
12155    fn write_computed_overlay_format_0based(
12156        &mut self,
12157        sheet: &str,
12158        row0: u32,
12159        col0: u32,
12160        format: Option<crate::format::FormatId>,
12161    ) {
12162        self.ensure_arrow_sheet(sheet);
12163        let (row0, col0) = (row0 as usize, col0 as usize);
12164        let Some(asheet) = self.arrow_sheets.sheet_mut(sheet) else {
12165            return;
12166        };
12167        if col0 >= asheet.columns.len() {
12168            asheet.insert_columns(asheet.columns.len(), col0 + 1 - asheet.columns.len());
12169        }
12170        if row0 >= asheet.nrows as usize {
12171            asheet.ensure_row_capacity(row0 + 1);
12172        }
12173        let Some((chunk, offset)) = asheet.chunk_of_row(row0) else {
12174            return;
12175        };
12176        if let Some(chunk) = asheet.ensure_column_chunk_mut(col0, chunk) {
12177            chunk.computed_overlay.set_format(offset, format);
12178        }
12179    }
12180
12181    /// One unbuffered computed write of `value` and its derived `format` at
12182    /// `cell` (a single sheet lookup; the same writes as
12183    /// `write_computed_overlay_value_0based` then
12184    /// `write_computed_overlay_format_0based`).
12185    fn write_computed_cell_0based(
12186        &mut self,
12187        cell: CellRef,
12188        value: &LiteralValue,
12189        format: Option<crate::format::FormatId>,
12190    ) {
12191        if !(self.config.arrow_storage_enabled
12192            && self.config.delta_overlay_enabled
12193            && self.config.write_formula_overlay_enabled)
12194            || self.computed_overlay_mirroring_disabled
12195        {
12196            return;
12197        }
12198        let sheet = self.graph.sheet_name(cell.sheet_id);
12199        let index = match self
12200            .arrow_sheets
12201            .sheets
12202            .iter()
12203            .position(|s| s.name.as_ref() == sheet)
12204        {
12205            Some(index) => index,
12206            None => {
12207                let sheet = sheet.to_string();
12208                self.ensure_arrow_sheet(&sheet);
12209                self.arrow_sheets.sheets.len() - 1
12210            }
12211        };
12212        let (row0, col0) = (cell.coord.row() as usize, cell.coord.col() as usize);
12213        let asheet = &mut self.arrow_sheets.sheets[index];
12214        let ov = Self::literal_to_overlay_value(value, asheet.date_system);
12215        let cur_cols = asheet.columns.len();
12216        if col0 >= cur_cols {
12217            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12218        }
12219        if row0 >= asheet.nrows as usize {
12220            asheet.ensure_row_capacity(row0 + 1);
12221        }
12222        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
12223            return;
12224        };
12225        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
12226            return;
12227        };
12228        let delta = ch.computed_overlay.set_scalar(in_off, ov);
12229        ch.computed_overlay.set_format(in_off, format);
12230        self.adjust_computed_overlay_bytes(delta);
12231        if let Some(cap) = self.config.max_overlay_memory_bytes
12232            && self.computed_overlay_bytes_estimate > cap
12233        {
12234            self.disable_computed_overlay_mirroring_due_to_budget(cap);
12235        }
12236    }
12237
12238    fn write_computed_overlay_value_0based(
12239        &mut self,
12240        sheet: &str,
12241        row0: u32,
12242        col0: u32,
12243        value: OverlayValue,
12244    ) {
12245        if !(self.config.arrow_storage_enabled
12246            && self.config.delta_overlay_enabled
12247            && self.config.write_formula_overlay_enabled)
12248        {
12249            return;
12250        }
12251        if self.computed_overlay_mirroring_disabled {
12252            return;
12253        }
12254
12255        self.ensure_arrow_sheet(sheet);
12256
12257        let row0 = row0 as usize;
12258        let col0 = col0 as usize;
12259        let asheet = self
12260            .arrow_sheets
12261            .sheet_mut(sheet)
12262            .expect("ArrowSheet must exist");
12263
12264        let cur_cols = asheet.columns.len();
12265        if col0 >= cur_cols {
12266            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12267        }
12268
12269        if row0 >= asheet.nrows as usize {
12270            if asheet.columns.is_empty() {
12271                asheet.insert_columns(0, 1);
12272            }
12273            asheet.ensure_row_capacity(row0 + 1);
12274        }
12275
12276        let Some((ch_idx, in_off)) = asheet.chunk_of_row(row0) else {
12277            return;
12278        };
12279        let Some(ch) = asheet.ensure_column_chunk_mut(col0, ch_idx) else {
12280            return;
12281        };
12282
12283        let delta = ch.computed_overlay.set_scalar(in_off, value);
12284        self.adjust_computed_overlay_bytes(delta);
12285
12286        if let Some(cap) = self.config.max_overlay_memory_bytes
12287            && self.computed_overlay_bytes_estimate > cap
12288        {
12289            self.disable_computed_overlay_mirroring_due_to_budget(cap);
12290        }
12291    }
12292
12293    pub(crate) fn plan_computed_write_coalescing(
12294        &self,
12295        buffer: &ComputedWriteBuffer,
12296    ) -> ComputedWriteCoalescingPlan {
12297        self.plan_computed_write_coalescing_from_writes(
12298            buffer.writes().iter().cloned(),
12299            buffer.formats_present,
12300        )
12301    }
12302
12303    fn plan_owned_computed_write_coalescing(
12304        &self,
12305        writes: Vec<ComputedWrite>,
12306        formats_present: bool,
12307    ) -> ComputedWriteCoalescingPlan {
12308        self.plan_computed_write_coalescing_from_writes(writes, formats_present)
12309    }
12310
12311    fn plan_computed_write_coalescing_from_writes(
12312        &self,
12313        writes: impl IntoIterator<Item = ComputedWrite>,
12314        formats_present: bool,
12315    ) -> ComputedWriteCoalescingPlan {
12316        let mut groups: BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>> =
12317            BTreeMap::new();
12318        let mut input_cells = 0usize;
12319        // One sheet lookup per sheet run of writes, not per cell.
12320        let mut located: Option<(SheetId, Option<&crate::arrow_store::ArrowSheet>)> = None;
12321
12322        for write in writes {
12323            match write {
12324                ComputedWrite::Cell {
12325                    seq,
12326                    sheet_id,
12327                    row0,
12328                    col0,
12329                    value,
12330                    format_id,
12331                } => {
12332                    input_cells = input_cells.saturating_add(1);
12333                    let sheet = match located {
12334                        Some((id, sheet)) if id == sheet_id => sheet,
12335                        _ => {
12336                            let sheet = self.arrow_sheets.sheet(self.graph.sheet_name(sheet_id));
12337                            located = Some((sheet_id, sheet));
12338                            sheet
12339                        }
12340                    };
12341                    let (chunk_idx, chunk_start_row0, row_in_chunk) = match sheet {
12342                        Some(sheet) => {
12343                            Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize)
12344                        }
12345                        None => Self::locate_row_in_empty_sheet_for_computed_write_plan(
12346                            row0 as usize,
12347                            32 * 1024,
12348                        ),
12349                    };
12350                    groups
12351                        .entry(ComputedWriteChunkKey {
12352                            sheet_id,
12353                            col0,
12354                            chunk_idx,
12355                            chunk_start_row0,
12356                        })
12357                        .or_default()
12358                        .push(ComputedWriteChunkEntryPlan {
12359                            row_in_chunk,
12360                            seq,
12361                            value,
12362                            format_id,
12363                        });
12364                }
12365                ComputedWrite::Run {
12366                    seq,
12367                    sheet_id,
12368                    row0,
12369                    col0,
12370                    entries,
12371                } => {
12372                    input_cells = input_cells.saturating_add(entries.len());
12373                    let sheet = match located {
12374                        Some((id, sheet)) if id == sheet_id => sheet,
12375                        _ => {
12376                            let sheet = self.arrow_sheets.sheet(self.graph.sheet_name(sheet_id));
12377                            located = Some((sheet_id, sheet));
12378                            sheet
12379                        }
12380                    };
12381                    // Rows are located one by one (a binary search, no sheet
12382                    // lookup); the group map is touched once per chunk
12383                    // segment of the run.
12384                    let mut segment: Vec<ComputedWriteChunkEntryPlan> = Vec::new();
12385                    let mut segment_key: Option<ComputedWriteChunkKey> = None;
12386                    for (k, (value, format_id)) in entries.into_iter().enumerate() {
12387                        let row = row0.saturating_add(k as u32) as usize;
12388                        let (chunk_idx, chunk_start_row0, row_in_chunk) = match sheet {
12389                            Some(sheet) => {
12390                                Self::locate_row_in_sheet_for_computed_write_plan(sheet, row)
12391                            }
12392                            None => Self::locate_row_in_empty_sheet_for_computed_write_plan(
12393                                row,
12394                                32 * 1024,
12395                            ),
12396                        };
12397                        let key = ComputedWriteChunkKey {
12398                            sheet_id,
12399                            col0,
12400                            chunk_idx,
12401                            chunk_start_row0,
12402                        };
12403                        if segment_key != Some(key)
12404                            && let Some(done) = segment_key.replace(key)
12405                        {
12406                            groups.entry(done).or_default().append(&mut segment);
12407                        }
12408                        segment.push(ComputedWriteChunkEntryPlan {
12409                            row_in_chunk,
12410                            seq,
12411                            value,
12412                            format_id,
12413                        });
12414                    }
12415                    if let Some(done) = segment_key {
12416                        groups.entry(done).or_default().append(&mut segment);
12417                    }
12418                }
12419                ComputedWrite::Rect {
12420                    seq,
12421                    sheet_id,
12422                    sr0,
12423                    sc0,
12424                    values,
12425                } => {
12426                    for (r_off, row) in values.into_iter().enumerate() {
12427                        for (c_off, value) in row.into_iter().enumerate() {
12428                            input_cells = input_cells.saturating_add(1);
12429                            self.push_computed_write_plan_entry(
12430                                &mut groups,
12431                                seq,
12432                                sheet_id,
12433                                sr0.saturating_add(r_off as u32),
12434                                sc0.saturating_add(c_off as u32),
12435                                value,
12436                                None,
12437                            );
12438                        }
12439                    }
12440                }
12441            }
12442        }
12443
12444        let mut plan = ComputedWriteCoalescingPlan {
12445            chunks: Vec::with_capacity(groups.len()),
12446            input_cells,
12447            coalesced_cells: 0,
12448            overwritten_cells: 0,
12449        };
12450        for (key, entries) in groups {
12451            let computed_lane_has_formats =
12452                self.computed_overlay_chunk_has_formats(key.sheet_id, key.col0, key.chunk_idx);
12453            let (chunk_plan, overwritten) = ComputedWriteChunkPlan::from_group(
12454                key,
12455                entries,
12456                formats_present,
12457                computed_lane_has_formats,
12458            );
12459            #[cfg(test)]
12460            if matches!(
12461                &chunk_plan.format_effect,
12462                ComputedWriteChunkFormatEffect::SetExplicit(_)
12463            ) {
12464                self.computed_format_vector_allocations_for_test
12465                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
12466            }
12467            plan.coalesced_cells = plan
12468                .coalesced_cells
12469                .saturating_add(chunk_plan.entries.len());
12470            plan.overwritten_cells = plan.overwritten_cells.saturating_add(overwritten);
12471            plan.chunks.push(chunk_plan);
12472        }
12473        debug_assert_eq!(
12474            plan.input_cells,
12475            plan.coalesced_cells.saturating_add(plan.overwritten_cells)
12476        );
12477        plan
12478    }
12479
12480    fn push_computed_write_plan_entry(
12481        &self,
12482        groups: &mut BTreeMap<ComputedWriteChunkKey, Vec<ComputedWriteChunkEntryPlan>>,
12483        seq: u64,
12484        sheet_id: SheetId,
12485        row0: u32,
12486        col0: u32,
12487        value: OverlayValue,
12488        format_id: Option<crate::format::FormatId>,
12489    ) {
12490        let (chunk_idx, chunk_start_row0, row_in_chunk) =
12491            self.locate_computed_write_chunk(sheet_id, row0);
12492        let key = ComputedWriteChunkKey {
12493            sheet_id,
12494            col0,
12495            chunk_idx,
12496            chunk_start_row0,
12497        };
12498        groups
12499            .entry(key)
12500            .or_default()
12501            .push(ComputedWriteChunkEntryPlan {
12502                row_in_chunk,
12503                seq,
12504                value,
12505                format_id,
12506            });
12507    }
12508
12509    fn computed_overlay_chunk_has_formats(
12510        &self,
12511        sheet_id: SheetId,
12512        col0: u32,
12513        chunk_idx: usize,
12514    ) -> bool {
12515        let sheet_name = self.graph.sheet_name(sheet_id);
12516        self.arrow_sheets
12517            .sheet(sheet_name)
12518            .and_then(|sheet| sheet.columns.get(col0 as usize))
12519            .and_then(|column| column.chunk(chunk_idx))
12520            .is_some_and(|chunk| chunk.computed_overlay.has_formats())
12521    }
12522
12523    fn locate_computed_write_chunk(&self, sheet_id: SheetId, row0: u32) -> (usize, u32, usize) {
12524        let sheet_name = self.graph.sheet_name(sheet_id);
12525        if let Some(sheet) = self.arrow_sheets.sheet(sheet_name) {
12526            return Self::locate_row_in_sheet_for_computed_write_plan(sheet, row0 as usize);
12527        }
12528        Self::locate_row_in_empty_sheet_for_computed_write_plan(row0 as usize, 32 * 1024)
12529    }
12530
12531    fn locate_row_in_sheet_for_computed_write_plan(
12532        sheet: &crate::arrow_store::ArrowSheet,
12533        row0: usize,
12534    ) -> (usize, u32, usize) {
12535        if row0 < sheet.nrows as usize
12536            && let Some((chunk_idx, row_in_chunk)) = sheet.chunk_of_row(row0)
12537        {
12538            let chunk_start = sheet.chunk_starts.get(chunk_idx).copied().unwrap_or(0);
12539            return (chunk_idx, chunk_start as u32, row_in_chunk);
12540        }
12541
12542        let chunk_rows = sheet.chunk_rows.max(1);
12543        if sheet.chunk_starts.is_empty() {
12544            return Self::locate_row_in_empty_sheet_for_computed_write_plan(row0, chunk_rows);
12545        }
12546
12547        let mut chunk_idx = sheet.chunk_starts.len().saturating_sub(1);
12548        let mut chunk_start = sheet.chunk_starts[chunk_idx];
12549        while chunk_start.saturating_add(chunk_rows) <= row0 {
12550            chunk_idx = chunk_idx.saturating_add(1);
12551            chunk_start = chunk_start.saturating_add(chunk_rows);
12552        }
12553        (
12554            chunk_idx,
12555            chunk_start as u32,
12556            row0.saturating_sub(chunk_start),
12557        )
12558    }
12559
12560    fn locate_row_in_empty_sheet_for_computed_write_plan(
12561        row0: usize,
12562        chunk_rows: usize,
12563    ) -> (usize, u32, usize) {
12564        let chunk_rows = chunk_rows.max(1);
12565        let chunk_idx = row0 / chunk_rows;
12566        let chunk_start = chunk_idx.saturating_mul(chunk_rows);
12567        (
12568            chunk_idx,
12569            chunk_start as u32,
12570            row0.saturating_sub(chunk_start),
12571        )
12572    }
12573
12574    #[cfg(test)]
12575    pub(crate) fn debug_plan_computed_write_coalescing(
12576        &self,
12577        buffer: &ComputedWriteBuffer,
12578    ) -> ComputedWriteCoalescingPlan {
12579        self.plan_computed_write_coalescing(buffer)
12580    }
12581
12582    pub(crate) fn flush_computed_write_buffer(
12583        &mut self,
12584        buffer: &mut ComputedWriteBuffer,
12585    ) -> Result<(), ExcelError> {
12586        if buffer.is_empty() {
12587            return Ok(());
12588        }
12589
12590        // Keep ownership of all pending writes until the final request
12591        // checkpoint and bounded commit-window preflight succeed so failures
12592        // remain retry safe. The synchronization and flush that follow are
12593        // infallible mutations with no cancellation point.
12594        self.resource_checkpoint(0)?;
12595        let commit_started = self.preflight_evaluation_commit_window(buffer.len())?;
12596        let (writes, formats_present) = buffer.take_writes();
12597        let plan = self.plan_owned_computed_write_coalescing(writes, formats_present);
12598        self.flush_computed_write_plan(plan);
12599        self.observe_evaluation_commit_window(commit_started);
12600
12601        Ok(())
12602    }
12603
12604    fn flush_computed_write_plan(&mut self, plan: ComputedWriteCoalescingPlan) {
12605        for chunk in plan.chunks {
12606            self.flush_computed_write_chunk_plan(chunk);
12607        }
12608    }
12609
12610    fn flush_computed_write_chunk_plan(&mut self, chunk: ComputedWriteChunkPlan) {
12611        match &chunk.shape {
12612            ComputedWriteChunkPlanShape::Point => {
12613                self.flush_computed_write_chunk_plan_as_points(chunk);
12614            }
12615            ComputedWriteChunkPlanShape::SparseOffsets { .. } => {
12616                self.flush_computed_write_chunk_plan_as_sparse_fragment_or_points(chunk);
12617            }
12618            ComputedWriteChunkPlanShape::DenseRange { .. } => {
12619                self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
12620            }
12621            ComputedWriteChunkPlanShape::RunRange { len, runs, .. } => {
12622                if Self::should_emit_computed_run_fragment(*len, *runs) {
12623                    self.flush_computed_write_chunk_plan_as_run_fragment(chunk);
12624                } else {
12625                    self.flush_computed_write_chunk_plan_as_dense_fragment(chunk);
12626                }
12627            }
12628        }
12629    }
12630
12631    #[inline]
12632    fn should_emit_computed_run_fragment(len: usize, runs: usize) -> bool {
12633        runs <= len / 2
12634    }
12635
12636    fn flush_computed_write_chunk_plan_as_points(&mut self, chunk: ComputedWriteChunkPlan) {
12637        let sheet_name = self.graph.sheet_name(chunk.sheet_id).to_string();
12638        for entry in chunk.entries {
12639            let row0 = chunk
12640                .chunk_start_row0
12641                .saturating_add(entry.row_in_chunk as u32);
12642            self.write_computed_overlay_value_0based(&sheet_name, row0, chunk.col0, entry.value);
12643        }
12644        self.apply_computed_overlay_format_effect(
12645            chunk.sheet_id,
12646            chunk.col0,
12647            chunk.chunk_idx,
12648            chunk.format_effect,
12649        );
12650    }
12651
12652    fn flush_computed_write_chunk_plan_as_sparse_fragment_or_points(
12653        &mut self,
12654        chunk: ComputedWriteChunkPlan,
12655    ) {
12656        let point_estimate = Self::computed_write_chunk_plan_point_estimate(&chunk);
12657        let sheet_id = chunk.sheet_id;
12658        let col0 = chunk.col0;
12659        let chunk_idx = chunk.chunk_idx;
12660        let chunk_start_row0 = chunk.chunk_start_row0;
12661        let format_effect = chunk.format_effect;
12662        let items: Vec<(usize, OverlayValue)> = chunk
12663            .entries
12664            .into_iter()
12665            .map(|entry| (entry.row_in_chunk, entry.value))
12666            .collect();
12667        match OverlayFragment::sparse_offsets_if_estimated_smaller_than_points(
12668            items,
12669            point_estimate,
12670        ) {
12671            Some(Ok(fragment)) => {
12672                self.apply_computed_overlay_fragment(sheet_id, col0, chunk_idx, fragment);
12673            }
12674            Some(Err(cells)) => {
12675                self.flush_computed_overlay_cells_as_points(
12676                    sheet_id,
12677                    col0,
12678                    chunk_start_row0,
12679                    cells,
12680                );
12681            }
12682            None => {}
12683        }
12684        self.apply_computed_overlay_format_effect(sheet_id, col0, chunk_idx, format_effect);
12685    }
12686
12687    #[inline]
12688    fn computed_write_chunk_plan_point_estimate(chunk: &ComputedWriteChunkPlan) -> usize {
12689        chunk
12690            .entries
12691            .iter()
12692            .map(|entry| ComputedWriteBuffer::estimate_value_bytes(&entry.value))
12693            .fold(0usize, usize::saturating_add)
12694    }
12695
12696    fn flush_computed_overlay_cells_as_points(
12697        &mut self,
12698        sheet_id: SheetId,
12699        col0: u32,
12700        chunk_start_row0: u32,
12701        cells: Vec<(usize, OverlayValue)>,
12702    ) {
12703        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
12704        for (row_in_chunk, value) in cells {
12705            let row0 = chunk_start_row0.saturating_add(row_in_chunk as u32);
12706            self.write_computed_overlay_value_0based(&sheet_name, row0, col0, value);
12707        }
12708    }
12709
12710    fn flush_computed_write_chunk_plan_as_dense_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
12711        if chunk.entries.is_empty() {
12712            return;
12713        }
12714        let start = chunk.entries[0].row_in_chunk;
12715        let values: Vec<OverlayValue> =
12716            chunk.entries.into_iter().map(|entry| entry.value).collect();
12717        if let Some(fragment) = OverlayFragment::dense_range(start, values) {
12718            self.apply_computed_overlay_fragment(
12719                chunk.sheet_id,
12720                chunk.col0,
12721                chunk.chunk_idx,
12722                fragment,
12723            );
12724        }
12725        self.apply_computed_overlay_format_effect(
12726            chunk.sheet_id,
12727            chunk.col0,
12728            chunk.chunk_idx,
12729            chunk.format_effect,
12730        );
12731    }
12732
12733    fn flush_computed_write_chunk_plan_as_run_fragment(&mut self, chunk: ComputedWriteChunkPlan) {
12734        if chunk.entries.is_empty() {
12735            return;
12736        }
12737        let start = chunk.entries[0].row_in_chunk;
12738        let values: Vec<OverlayValue> =
12739            chunk.entries.into_iter().map(|entry| entry.value).collect();
12740        if let Some(fragment) = OverlayFragment::run_range(start, values) {
12741            self.apply_computed_overlay_fragment(
12742                chunk.sheet_id,
12743                chunk.col0,
12744                chunk.chunk_idx,
12745                fragment,
12746            );
12747        }
12748        self.apply_computed_overlay_format_effect(
12749            chunk.sheet_id,
12750            chunk.col0,
12751            chunk.chunk_idx,
12752            chunk.format_effect,
12753        );
12754    }
12755
12756    fn apply_computed_overlay_format_effect(
12757        &mut self,
12758        sheet_id: SheetId,
12759        col0: u32,
12760        chunk_idx: usize,
12761        effect: ComputedWriteChunkFormatEffect,
12762    ) {
12763        if !(self.config.arrow_storage_enabled
12764            && self.config.delta_overlay_enabled
12765            && self.config.write_formula_overlay_enabled)
12766            || self.computed_overlay_mirroring_disabled
12767        {
12768            return;
12769        }
12770
12771        let sheet_name = self.graph.sheet_name(sheet_id);
12772        let Some(sheet) = self.arrow_sheets.sheet_mut(sheet_name) else {
12773            return;
12774        };
12775        let Some(chunk) = sheet
12776            .columns
12777            .get_mut(col0 as usize)
12778            .and_then(|column| column.chunk_mut(chunk_idx))
12779        else {
12780            return;
12781        };
12782        match effect {
12783            ComputedWriteChunkFormatEffect::NoFormatWork => {}
12784            ComputedWriteChunkFormatEffect::ClearStale(ComputedWriteFormatClear::Range {
12785                start,
12786                end,
12787            }) => {
12788                #[cfg(test)]
12789                {
12790                    self.computed_overlay_stale_clear_range_effects_for_test = self
12791                        .computed_overlay_stale_clear_range_effects_for_test
12792                        .saturating_add(1);
12793                }
12794                chunk.computed_overlay.clear_format_range(start, end);
12795            }
12796            ComputedWriteChunkFormatEffect::ClearStale(ComputedWriteFormatClear::Offsets(
12797                offsets,
12798            )) => {
12799                #[cfg(test)]
12800                {
12801                    self.computed_overlay_stale_clear_offset_attempts_for_test = self
12802                        .computed_overlay_stale_clear_offset_attempts_for_test
12803                        .saturating_add(offsets.len() as u64);
12804                }
12805                chunk.computed_overlay.clear_format_offsets(&offsets);
12806            }
12807            ComputedWriteChunkFormatEffect::SetExplicit(formats) => {
12808                #[cfg(test)]
12809                {
12810                    self.computed_overlay_set_explicit_entry_operations_for_test = self
12811                        .computed_overlay_set_explicit_entry_operations_for_test
12812                        .saturating_add(formats.len() as u64);
12813                }
12814                for (row_in_chunk, format_id) in formats {
12815                    chunk.computed_overlay.set_format(row_in_chunk, format_id);
12816                }
12817            }
12818        }
12819    }
12820
12821    fn apply_computed_overlay_fragment(
12822        &mut self,
12823        sheet_id: SheetId,
12824        col0: u32,
12825        chunk_idx: usize,
12826        fragment: OverlayFragment,
12827    ) {
12828        if !(self.config.arrow_storage_enabled
12829            && self.config.delta_overlay_enabled
12830            && self.config.write_formula_overlay_enabled)
12831        {
12832            return;
12833        }
12834        if self.computed_overlay_mirroring_disabled {
12835            return;
12836        }
12837
12838        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
12839        self.ensure_arrow_sheet(&sheet_name);
12840
12841        let col0 = col0 as usize;
12842        let asheet = self
12843            .arrow_sheets
12844            .sheet_mut(&sheet_name)
12845            .expect("ArrowSheet must exist");
12846
12847        let cur_cols = asheet.columns.len();
12848        if col0 >= cur_cols {
12849            asheet.insert_columns(cur_cols, (col0 + 1) - cur_cols);
12850        }
12851
12852        let start_row0 = asheet
12853            .chunk_starts
12854            .get(chunk_idx)
12855            .copied()
12856            .unwrap_or_else(|| chunk_idx.saturating_mul(asheet.chunk_rows.max(1)));
12857        let required_rows =
12858            start_row0.saturating_add(fragment.max_covered_offset().saturating_add(1));
12859        if required_rows > asheet.nrows as usize {
12860            if asheet.columns.is_empty() {
12861                asheet.insert_columns(0, 1);
12862            }
12863            asheet.ensure_row_capacity(required_rows);
12864        }
12865
12866        let Some(ch) = asheet.ensure_column_chunk_mut(col0, chunk_idx) else {
12867            return;
12868        };
12869        let delta = ch.computed_overlay.apply_fragment(fragment);
12870        self.adjust_computed_overlay_bytes(delta);
12871
12872        if let Some(cap) = self.config.max_overlay_memory_bytes
12873            && self.computed_overlay_bytes_estimate > cap
12874        {
12875            self.disable_computed_overlay_mirroring_due_to_budget(cap);
12876        }
12877    }
12878
12879    #[inline]
12880    fn adjust_computed_overlay_bytes(&mut self, delta: isize) {
12881        if delta >= 0 {
12882            self.computed_overlay_bytes_estimate = self
12883                .computed_overlay_bytes_estimate
12884                .saturating_add(delta as usize);
12885        } else {
12886            self.computed_overlay_bytes_estimate = self
12887                .computed_overlay_bytes_estimate
12888                .saturating_sub((-delta) as usize);
12889        }
12890    }
12891
12892    fn clear_all_computed_overlays(&mut self) {
12893        let mut freed_total = 0usize;
12894        for sh in self.arrow_sheets.sheets.iter_mut() {
12895            for col in sh.columns.iter_mut() {
12896                for ch in col.chunks.iter_mut() {
12897                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
12898                }
12899                for ch in col.sparse_chunks.values_mut() {
12900                    freed_total = freed_total.saturating_add(ch.computed_overlay.clear());
12901                }
12902            }
12903        }
12904        self.computed_overlay_bytes_estimate = self
12905            .computed_overlay_bytes_estimate
12906            .saturating_sub(freed_total);
12907    }
12908
12909    fn disable_computed_overlay_mirroring_due_to_budget(&mut self, _cap: usize) {
12910        // Phase 1 (ticket 610): Arrow-truth is the only supported mode.
12911        // Handle budget pressure by compacting computed overlays into base lanes.
12912        self.compact_all_computed_overlays();
12913    }
12914
12915    /// Fold all computed overlay entries across all sheets into their base arrays.
12916    /// This preserves data while freeing overlay memory, allowing mirroring to continue.
12917    fn compact_all_computed_overlays(&mut self) {
12918        let mut freed_total = 0usize;
12919        for sheet in self.arrow_sheets.sheets.iter_mut() {
12920            for col_idx in 0..sheet.columns.len() {
12921                // Dense chunks
12922                let num_dense = sheet.columns[col_idx].chunks.len();
12923                for ch_idx in 0..num_dense {
12924                    freed_total += sheet.compact_computed_overlay_chunk(col_idx, ch_idx);
12925                }
12926                // Sparse chunks
12927                let sparse_keys: Vec<usize> = sheet.columns[col_idx]
12928                    .sparse_chunks
12929                    .keys()
12930                    .copied()
12931                    .collect();
12932                for ch_idx in sparse_keys {
12933                    freed_total += sheet.compact_computed_overlay_sparse_chunk(col_idx, ch_idx);
12934                }
12935            }
12936        }
12937        self.computed_overlay_bytes_estimate = self
12938            .computed_overlay_bytes_estimate
12939            .saturating_sub(freed_total);
12940        self.overlay_compactions = self.overlay_compactions.saturating_add(1);
12941    }
12942
12943    fn mirror_vertex_value_to_overlay(&mut self, vertex_id: VertexId, value: &LiteralValue) {
12944        let _ = self.record_vertex_value_to_overlay(vertex_id, value, None);
12945    }
12946
12947    fn record_vertex_value_to_overlay(
12948        &mut self,
12949        vertex_id: VertexId,
12950        value: &LiteralValue,
12951        computed_writes: Option<&mut ComputedWriteBuffer>,
12952    ) -> Result<(), ExcelError> {
12953        if !(self.config.arrow_storage_enabled
12954            && self.config.delta_overlay_enabled
12955            && self.config.write_formula_overlay_enabled)
12956        {
12957            return Ok(());
12958        }
12959        if self.computed_overlay_mirroring_disabled {
12960            return Ok(());
12961        }
12962        if !matches!(
12963            self.graph.get_vertex_kind(vertex_id),
12964            VertexKind::FormulaScalar | VertexKind::FormulaArray
12965        ) {
12966            return Ok(());
12967        }
12968        let Some(cell) = self.graph.get_cell_ref(vertex_id) else {
12969            return Ok(());
12970        };
12971        let Some(buffer) = computed_writes else {
12972            // Unbuffered: one sheet lookup for the value and its format.
12973            let format_id = self.derived_formats.get(&cell);
12974            self.write_computed_cell_0based(cell, value, format_id);
12975            return Ok(());
12976        };
12977        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
12978        let date_system = self.arrow_sheet_date_system(&sheet_name);
12979        let ov = Self::literal_to_overlay_value(value, date_system);
12980        {
12981            let format_id = self.derived_formats.get(&cell);
12982            buffer.push_cell_with_format(
12983                cell.sheet_id,
12984                cell.coord.row(),
12985                cell.coord.col(),
12986                ov,
12987                format_id,
12988            );
12989            if self.should_flush_computed_write_buffer(buffer) {
12990                self.flush_computed_write_buffer(buffer)?;
12991            }
12992        }
12993        Ok(())
12994    }
12995
12996    #[inline]
12997    fn should_flush_computed_write_buffer(&self, buffer: &ComputedWriteBuffer) -> bool {
12998        self.config.max_overlay_memory_bytes.is_some_and(|cap| {
12999            if cap == 0 {
13000                return false;
13001            }
13002            self.computed_overlay_bytes_estimate
13003                .saturating_add(buffer.estimated_bytes())
13004                > cap
13005        })
13006    }
13007
13008    /// Estimated memory usage for computed overlays (formula/spill mirroring).
13009    pub fn overlay_memory_usage(&self) -> usize {
13010        self.computed_overlay_bytes_estimate
13011    }
13012
13013    #[cfg(test)]
13014    pub(crate) fn debug_overlay_compactions(&self) -> u64 {
13015        self.overlay_compactions
13016    }
13017
13018    #[cfg(test)]
13019    pub(crate) fn debug_recompute_computed_overlay_bytes(&mut self) -> usize {
13020        let mut total = 0usize;
13021        for sheet in &self.arrow_sheets.sheets {
13022            for column in &sheet.columns {
13023                for chunk in &column.chunks {
13024                    total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
13025                }
13026                for chunk in column.sparse_chunks.values() {
13027                    total = total.saturating_add(chunk.computed_overlay.estimated_bytes());
13028                }
13029            }
13030        }
13031        self.computed_overlay_bytes_estimate = total;
13032        total
13033    }
13034
13035    fn resolve_sheet_locator_for_write(
13036        &mut self,
13037        loc: formualizer_common::SheetLocator<'_>,
13038        current_sheet: &str,
13039    ) -> Result<SheetId, ExcelError> {
13040        Ok(match loc {
13041            formualizer_common::SheetLocator::Id(id) => id,
13042            formualizer_common::SheetLocator::Name(name) => self.graph.sheet_id_mut(name.as_ref()),
13043            formualizer_common::SheetLocator::Current => self.graph.sheet_id_mut(current_sheet),
13044        })
13045    }
13046
13047    fn resolve_sheet_locator_for_read(
13048        &self,
13049        loc: formualizer_common::SheetLocator<'_>,
13050        current_sheet: &str,
13051    ) -> Result<SheetId, ExcelError> {
13052        match loc {
13053            formualizer_common::SheetLocator::Id(id) => Ok(id),
13054            formualizer_common::SheetLocator::Name(name) => self
13055                .graph
13056                .sheet_id(name.as_ref())
13057                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
13058            formualizer_common::SheetLocator::Current => self
13059                .graph
13060                .sheet_id(current_sheet)
13061                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref)),
13062        }
13063    }
13064
13065    /// Set a cell value
13066    pub fn set_cell_value(
13067        &mut self,
13068        sheet: &str,
13069        row: u32,
13070        col: u32,
13071        value: LiteralValue,
13072    ) -> Result<(), ExcelError> {
13073        self.observe_function_semantic_epoch()?;
13074        let sheet_existed = self.graph.sheet_id(sheet).is_some();
13075        let sheet_id = self.graph.sheet_id_mut(sheet);
13076        let cell_ref = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13077        let replaced = self
13078            .graph
13079            .get_vertex_id_for_address(&cell_ref)
13080            .filter(|&vertex| {
13081                matches!(
13082                    self.graph.get_vertex_kind(vertex),
13083                    VertexKind::FormulaScalar | VertexKind::FormulaArray
13084                )
13085            });
13086        let replaced_formula = replaced.is_some();
13087        // A value replacing a spilled anchor takes its spill with it (no
13088        // evaluation will revisit the anchor); anchors that spill blocked
13089        // wake through the cleared region.
13090        if let Some(anchor) = replaced
13091            && self.graph.spill_registry_has_anchor(anchor)
13092        {
13093            self.clear_spill_projection_and_mirror(anchor, None);
13094        }
13095        self.graph.set_cell_value(sheet, row, col, value.clone())?;
13096        self.clear_cell_format_state(sheet, cell_ref);
13097        self.record_changed_cell(sheet, row, col);
13098        if !sheet_existed || replaced_formula {
13099            self.mark_topology_edited();
13100        }
13101        // Mirror into Arrow overlay when enabled
13102        self.mirror_value_to_overlay(sheet, row, col, &value);
13103        // Advance snapshot to reflect external mutation.
13104        self.mark_data_edited();
13105        Ok(())
13106    }
13107
13108    /// Record a single-cell change: invalidates pending spills blocked on it.
13109    fn record_changed_cell(&mut self, sheet: &str, row: u32, col: u32) {
13110        let sheet_id = self.graph.sheet_id_mut(sheet);
13111        self.record_structural_change(StructuralScope::Cell {
13112            sheet: sheet_id,
13113            row: row.saturating_sub(1),
13114            col: col.saturating_sub(1),
13115        });
13116    }
13117
13118    fn record_change_for_event(&mut self, event: &ChangeEvent) {
13119        match event {
13120            ChangeEvent::SetValue { addr, .. } | ChangeEvent::SetFormula { addr, .. } => {
13121                self.record_structural_change(StructuralScope::Cell {
13122                    sheet: addr.sheet_id,
13123                    row: addr.coord.row(),
13124                    col: addr.coord.col(),
13125                });
13126            }
13127            ChangeEvent::SpillCommitted { new, .. } => {
13128                if let Some(scope) = Self::structural_scope_from_cells(&new.target_cells) {
13129                    self.record_structural_change(scope);
13130                }
13131            }
13132            ChangeEvent::SpillCleared { old, .. } => {
13133                if let Some(scope) = Self::structural_scope_from_cells(&old.target_cells) {
13134                    self.record_structural_change(scope);
13135                }
13136            }
13137            ChangeEvent::DefineName { .. }
13138            | ChangeEvent::UpdateName { .. }
13139            | ChangeEvent::DeleteName { .. }
13140            | ChangeEvent::NamedRangeAdjusted { .. } => {
13141                // Direct name events are preflighted by the logged-name APIs.
13142                // Structural entry points preflight spans before emitting a
13143                // NamedRangeAdjusted event. Epoch changes only rebuild caches.
13144                self.record_structural_change(StructuralScope::AllSheets);
13145            }
13146            ChangeEvent::VertexMoved { .. } | ChangeEvent::FormulaAdjusted { .. } => {
13147                // Structural entry points publish their axis delta once after
13148                // the graph and Arrow commits.
13149            }
13150            ChangeEvent::SetRowVisibility { sheet_id, row0, .. } => {
13151                self.record_structural_change(StructuralScope::Region(Region::whole_row(
13152                    *sheet_id, *row0,
13153                )));
13154            }
13155            ChangeEvent::AddVertex { .. }
13156            | ChangeEvent::RemoveVertex { .. }
13157            | ChangeEvent::EdgeAdded { .. }
13158            | ChangeEvent::EdgeRemoved { .. }
13159            | ChangeEvent::CompoundStart { .. }
13160            | ChangeEvent::CompoundEnd { .. }
13161            | ChangeEvent::StagedFormulaCellChanged { .. } => {}
13162        }
13163    }
13164
13165    fn record_structural_change(&mut self, scope: StructuralScope) {
13166        if let StructuralScope::Cell { sheet, row, col } = scope {
13167            self.wake_spill_owner_of_entered_formula(sheet, row, col);
13168        }
13169        self.wake_spill_blocker_waiters(scope);
13170        self.invalidate_pending_spills(scope);
13171    }
13172
13173    /// Wake anchors waiting on a blocking cell inside `scope`, with their
13174    /// dependents: the anchor's value changes when it spills. A woken anchor
13175    /// registers again if it is still blocked.
13176    fn wake_spill_blocker_waiters(&mut self, scope: StructuralScope) {
13177        if self.spill_blocker_waiters.is_empty() {
13178            return;
13179        }
13180        let woken = self.spill_blocker_waiters.take_affected(scope);
13181        let woken: Vec<VertexId> = woken
13182            .into_iter()
13183            .filter(|&vertex| {
13184                self.graph.vertex_exists(vertex)
13185                    && matches!(
13186                        self.graph.get_vertex_kind(vertex),
13187                        VertexKind::FormulaScalar | VertexKind::FormulaArray
13188                    )
13189            })
13190            .collect();
13191        if !woken.is_empty() {
13192            self.graph.mark_dirty_many(&woken);
13193        }
13194    }
13195
13196    /// A formula entered into a live spill blocks it: wake the owning anchor
13197    /// so its next recalc re-plans (and reports `#SPILL!`). The spill keeps
13198    /// its registry until then; clearing it leaves the formula cell alone.
13199    fn wake_spill_owner_of_entered_formula(&mut self, sheet: SheetId, row: u32, col: u32) {
13200        let cell = self.graph.make_cell_ref_internal(sheet, row, col);
13201        if let Some(owner) = self.graph.spill_registry_anchor_for_cell(cell)
13202            && self.graph.is_foreign_formula_cell(&cell, owner)
13203        {
13204            self.graph.mark_dirty_many(&[owner]);
13205        }
13206    }
13207
13208    fn structural_scope_from_cells(cells: &[CellRef]) -> Option<StructuralScope> {
13209        let first = cells.first()?;
13210        let sheet_id = first.sheet_id;
13211        if cells.iter().any(|cell| cell.sheet_id != sheet_id) {
13212            return Some(StructuralScope::OpaqueGlobal);
13213        }
13214        let mut row_start = first.coord.row();
13215        let mut row_end = row_start;
13216        let mut col_start = first.coord.col();
13217        let mut col_end = col_start;
13218        for cell in cells.iter().skip(1) {
13219            row_start = row_start.min(cell.coord.row());
13220            row_end = row_end.max(cell.coord.row());
13221            col_start = col_start.min(cell.coord.col());
13222            col_end = col_end.max(cell.coord.col());
13223        }
13224        Some(StructuralScope::Region(Region::rect(
13225            sheet_id, row_start, row_end, col_start, col_end,
13226        )))
13227    }
13228
13229    pub fn set_cell_value_ref(
13230        &mut self,
13231        cell: formualizer_common::SheetCellRef<'_>,
13232        current_sheet: &str,
13233        value: LiteralValue,
13234    ) -> Result<(), ExcelError> {
13235        let owned = cell.into_owned();
13236        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
13237        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
13238        self.set_cell_value(
13239            &sheet_name,
13240            owned.coord.row() + 1,
13241            owned.coord.col() + 1,
13242            value,
13243        )
13244    }
13245
13246    pub fn set_cell_formula_ref(
13247        &mut self,
13248        cell: formualizer_common::SheetCellRef<'_>,
13249        current_sheet: &str,
13250        ast: ASTNode,
13251    ) -> Result<(), ExcelError> {
13252        let owned = cell.into_owned();
13253        let sheet_id = self.resolve_sheet_locator_for_write(owned.sheet, current_sheet)?;
13254        let sheet_name = self.graph.sheet_name(sheet_id).to_string();
13255        self.set_cell_formula(
13256            &sheet_name,
13257            owned.coord.row() + 1,
13258            owned.coord.col() + 1,
13259            ast,
13260        )
13261    }
13262
13263    pub fn get_cell_value_ref(
13264        &self,
13265        cell: formualizer_common::SheetCellRef<'_>,
13266        current_sheet: &str,
13267    ) -> Result<Option<LiteralValue>, ExcelError> {
13268        let owned = cell.into_owned();
13269        let sheet_id = self.resolve_sheet_locator_for_read(owned.sheet, current_sheet)?;
13270        let sheet_name = self.graph.sheet_name(sheet_id);
13271        Ok(self.get_cell_value(sheet_name, owned.coord.row() + 1, owned.coord.col() + 1))
13272    }
13273
13274    pub fn resolve_range_view_sheet_ref<'c>(
13275        &'c self,
13276        r: &formualizer_common::SheetRef<'_>,
13277        current_sheet: &str,
13278    ) -> Result<RangeView<'c>, ExcelError> {
13279        use formualizer_common::SheetLocator;
13280
13281        let sheet_to_opt_name = |loc: SheetLocator<'_>| -> Result<Option<String>, ExcelError> {
13282            match loc {
13283                SheetLocator::Current => Ok(None),
13284                SheetLocator::Name(name) => Ok(Some(name.as_ref().to_string())),
13285                SheetLocator::Id(id) => Ok(Some(self.graph.sheet_name(id).to_string())),
13286            }
13287        };
13288
13289        let rt = match r {
13290            formualizer_common::SheetRef::Cell(cell) => ReferenceType::Cell {
13291                sheet: sheet_to_opt_name(cell.sheet.clone())?,
13292                row: cell.coord.row() + 1,
13293                col: cell.coord.col() + 1,
13294                row_abs: cell.coord.row_abs(),
13295                col_abs: cell.coord.col_abs(),
13296            },
13297            formualizer_common::SheetRef::Range(range) => ReferenceType::Range {
13298                sheet: sheet_to_opt_name(range.sheet.clone())?,
13299                start_row: range.start_row.map(|b| b.index + 1),
13300                start_col: range.start_col.map(|b| b.index + 1),
13301                end_row: range.end_row.map(|b| b.index + 1),
13302                end_col: range.end_col.map(|b| b.index + 1),
13303                start_row_abs: range.start_row.map(|b| b.abs).unwrap_or(false),
13304                start_col_abs: range.start_col.map(|b| b.abs).unwrap_or(false),
13305                end_row_abs: range.end_row.map(|b| b.abs).unwrap_or(false),
13306                end_col_abs: range.end_col.map(|b| b.abs).unwrap_or(false),
13307            },
13308        };
13309
13310        crate::traits::EvaluationContext::resolve_range_view(self, &rt, current_sheet)
13311    }
13312
13313    /// Set a cell formula
13314    pub fn set_cell_formula(
13315        &mut self,
13316        sheet: &str,
13317        row: u32,
13318        col: u32,
13319        ast: ASTNode,
13320    ) -> Result<(), ExcelError> {
13321        self.observe_function_semantic_epoch()?;
13322        let sheet_id = self.graph.sheet_id_mut(sheet);
13323        let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13324        let ingested = {
13325            let mut pipeline = self.ingest_pipeline();
13326            pipeline.ingest_formula(FormulaAstInput::Tree(ast), placement, None)?
13327        };
13328        self.graph.set_cell_formula_with_plan(
13329            sheet,
13330            row,
13331            col,
13332            ingested.ast_id,
13333            &ingested.dep_plan,
13334            ingested.dep_plan.volatile,
13335            ingested.dep_plan.dynamic,
13336        )?;
13337        self.clear_cell_format_state(sheet, placement);
13338        self.record_changed_cell(sheet, row, col);
13339
13340        // If the cell previously held a user value in the delta overlay, it must not continue
13341        // to mask the formula result under Arrow-canonical reads (overlay precedence is
13342        // delta -> computed -> base). Remove the overlay entry instead of writing `Empty`,
13343        // because an explicit `Empty` overlay would still take precedence over computed values.
13344        self.clear_delta_overlay_cell(sheet, row, col);
13345
13346        // Advance snapshot to reflect external mutation
13347        self.mark_topology_edited();
13348        Ok(())
13349    }
13350
13351    /// Bulk set many formulas on a sheet. Skips per-cell snapshot bumping and minimizes edge rebuilds.
13352    pub fn bulk_set_formulas<I>(&mut self, sheet: &str, items: I) -> Result<usize, ExcelError>
13353    where
13354        I: IntoIterator<Item = (u32, u32, ASTNode)>,
13355    {
13356        let collected: Vec<(u32, u32, ASTNode)> = items.into_iter().collect();
13357        let edited_cells: Vec<(u32, u32)> = collected.iter().map(|(r, c, _)| (*r, *c)).collect();
13358        let sheet_id = self.graph.sheet_id_mut(sheet);
13359        let ingested = {
13360            let mut pipeline = self.ingest_pipeline();
13361            let inputs = collected.into_iter().map(|(row, col, ast)| {
13362                let placement = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13363                (FormulaAstInput::Tree(ast), placement, None)
13364            });
13365            pipeline.ingest_batch(inputs)?
13366        };
13367        let planned: Vec<(u32, u32, AstNodeId, DependencyPlanRow)> = ingested
13368            .into_iter()
13369            .map(|formula| {
13370                (
13371                    formula.placement.coord.row() + 1,
13372                    formula.placement.coord.col() + 1,
13373                    formula.ast_id,
13374                    formula.dep_plan,
13375                )
13376            })
13377            .collect();
13378        let n = self.graph.bulk_set_formulas_with_plans(sheet, planned)?;
13379        for (row, col) in edited_cells {
13380            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13381            self.clear_cell_format_state(sheet, cell);
13382            self.record_changed_cell(sheet, row, col);
13383        }
13384        // Single topology bump after batch
13385        if n > 0 {
13386            self.mark_topology_edited();
13387        }
13388        Ok(n)
13389    }
13390
13391    /// Declare the formula cell at `sheet!(row, col)` (1-based) a dynamic
13392    /// array anchor whose spill identity comes from the source document, for
13393    /// example an XLSX `t="array"` formula bound to XLDAPR cell metadata.
13394    ///
13395    /// This changes only spill references to the anchor (`A1#`,
13396    /// `_xlfn.ANCHORARRAY(A1)`). A declared anchor with a committed
13397    /// multi-cell spill resolves to that spill, exactly as an undeclared
13398    /// one. Without a committed spill, a declared anchor whose current
13399    /// result is a scalar (not an error, so not a blocked `#SPILL!`
13400    /// anchor, and not empty) resolves to its own cell as a 1x1 reference,
13401    /// matching a spilled 1x1 dynamic array. Every other case keeps the
13402    /// `#REF!` of an undeclared anchor. Undeclared cells are unaffected, and
13403    /// fresh 1x1 results are not given spill identity by function name.
13404    ///
13405    /// The declaration belongs to the formula vertex. It is dropped when the
13406    /// cell's formula is replaced, the cell is cleared or overwritten with a
13407    /// value, the vertex is removed (including with its sheet), or a
13408    /// structural row/column edit moves the vertex. A structural edit that
13409    /// only adjusts the formula's references, without moving the anchor,
13410    /// keeps it. Declaring again re-establishes it.
13411    ///
13412    /// The anchor and its dependents are marked dirty, so the next
13413    /// evaluation observes the declaration.
13414    ///
13415    /// Returns `#REF!` if the sheet does not exist or the cell holds no
13416    /// formula vertex. A formula still staged by deferred graph building has
13417    /// no vertex: build the graph (for example [`Self::build_graph_all`])
13418    /// before declaring.
13419    pub fn declare_dynamic_array_anchor(
13420        &mut self,
13421        sheet: &str,
13422        row: u32,
13423        col: u32,
13424    ) -> Result<(), ExcelError> {
13425        let not_a_formula = || {
13426            ExcelError::new(ExcelErrorKind::Ref)
13427                .with_message("Declared dynamic array anchor must be a formula cell")
13428        };
13429        if row == 0 || col == 0 {
13430            return Err(not_a_formula());
13431        }
13432        if self.get_staged_formula_text(sheet, row, col).is_some() {
13433            return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
13434                "Declared dynamic array anchor is still staged; build the graph first",
13435            ));
13436        }
13437        let sheet_id = self.graph.sheet_id(sheet).ok_or_else(not_a_formula)?;
13438        let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13439        let vertex = self
13440            .graph
13441            .cell_vertex_mut(&cell)
13442            .filter(|v| self.graph.vertex_has_formula(*v))
13443            .ok_or_else(not_a_formula)?;
13444        if self.graph.declare_dynamic_anchor(vertex, cell) {
13445            self.graph.mark_dirty_many(&[vertex]);
13446            self.mark_topology_edited();
13447        }
13448        Ok(())
13449    }
13450
13451    /// Declare a legacy array formula with a fixed output rectangle after
13452    /// ingestion and before evaluation. Coordinates are one-based. Replacement,
13453    /// removal or movement of the formula clears the declaration.
13454    /// Single-cell declarations do not register a spill anchor. Requires
13455    /// `EvalConfig::family_execution = false` to bypass family memoization.
13456    pub fn declare_fixed_array_formula(
13457        &mut self,
13458        sheet: &str,
13459        row: u32,
13460        col: u32,
13461        rows: u32,
13462        cols: u32,
13463    ) -> Result<(), ExcelError> {
13464        if self.config.family_execution {
13465            return Err(ExcelError::new(ExcelErrorKind::Ref)
13466                .with_message("Fixed arrays require family_execution = false"));
13467        }
13468        if row == 0
13469            || col == 0
13470            || rows == 0
13471            || cols == 0
13472            || row.checked_add(rows - 1).is_none_or(|r| r > 1_048_576)
13473            || col.checked_add(cols - 1).is_none_or(|c| c > 16_384)
13474            || u64::from(rows) * u64::from(cols) > u64::from(self.config.spill.max_spill_cells)
13475        {
13476            return Err(ExcelError::new(ExcelErrorKind::Ref)
13477                .with_message("Invalid or over-cap fixed array extent"));
13478        }
13479        if self.get_staged_formula_text(sheet, row, col).is_some() {
13480            return Err(ExcelError::new(ExcelErrorKind::Ref)
13481                .with_message("Fixed array formula is still staged; build the graph first"));
13482        }
13483        let sheet_id = self.graph.sheet_id(sheet).ok_or_else(|| {
13484            ExcelError::new(ExcelErrorKind::Ref).with_message("Unknown fixed array sheet")
13485        })?;
13486        let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13487        let vertex = self
13488            .graph
13489            .cell_vertex_mut(&cell)
13490            .filter(|v| self.graph.vertex_has_formula(*v))
13491            .ok_or_else(|| {
13492                ExcelError::new(ExcelErrorKind::Ref)
13493                    .with_message("Fixed array anchor must be a formula cell")
13494            })?;
13495        if (rows == 1 && cols == 1 && self.graph.fixed_single_arrays.contains(&vertex))
13496            || self.graph.fixed_array_shapes.get(&vertex) == Some(&(rows, cols))
13497        {
13498            return Ok(());
13499        }
13500        if self.graph.fixed_single_arrays.contains(&vertex)
13501            || self.graph.spill_registry_has_anchor(vertex)
13502            || self.graph.is_current_declared_dynamic_anchor(vertex)
13503        {
13504            return Err(ExcelError::new(ExcelErrorKind::Ref)
13505                .with_message("Array declaration already exists; replace the formula first"));
13506        }
13507        if rows == 1 && cols == 1 {
13508            self.graph.fixed_single_arrays.insert(vertex);
13509        } else {
13510            let mut targets = Vec::with_capacity((rows * cols) as usize);
13511            for r in 0..rows {
13512                for c in 0..cols {
13513                    targets.push(self.graph.make_cell_ref(sheet, row + r, col + c));
13514                }
13515            }
13516            self.graph.plan_spill_region(vertex, &targets)?;
13517            // Reserve ownership before evaluation, even for Empty members.
13518            self.graph
13519                .commit_spill_region_atomic_with_fault(vertex, targets, vec![], None)?;
13520            self.graph.fixed_array_shapes.insert(vertex, (rows, cols));
13521        }
13522        self.graph.mark_dirty_many(&[vertex]);
13523        self.mark_topology_edited();
13524        Ok(())
13525    }
13526
13527    #[inline]
13528    fn normalize_public_cell_read(v: LiteralValue) -> Option<LiteralValue> {
13529        match v {
13530            LiteralValue::Empty => None,
13531            LiteralValue::Int(i) => Some(LiteralValue::Number(i as f64)),
13532            other => Some(other),
13533        }
13534    }
13535
13536    fn materialize_temporal_egress(
13537        value: LiteralValue,
13538        class: Option<&formualizer_common::numfmt::FormatClass>,
13539        policy: crate::engine::TemporalEgress,
13540        date_system: crate::engine::DateSystem,
13541    ) -> LiteralValue {
13542        use formualizer_common::numfmt::FormatClass;
13543        if policy == crate::engine::TemporalEgress::Serial {
13544            return value;
13545        }
13546        let LiteralValue::Number(serial) = value else {
13547            return value;
13548        };
13549        match class {
13550            Some(FormatClass::Date) => {
13551                formualizer_common::try_serial_to_date_for(date_system, serial)
13552                    .map(LiteralValue::Date)
13553                    .unwrap_or(LiteralValue::Number(serial))
13554            }
13555            Some(FormatClass::DateTime) => {
13556                formualizer_common::try_serial_to_datetime_for(date_system, serial)
13557                    .map(LiteralValue::DateTime)
13558                    .unwrap_or(LiteralValue::Number(serial))
13559            }
13560            Some(FormatClass::Time) => {
13561                let seconds = (serial.rem_euclid(1.0) * 86_400.0).round() as u32 % 86_400;
13562                chrono::NaiveTime::from_num_seconds_from_midnight_opt(seconds, 0)
13563                    .map(LiteralValue::Time)
13564                    .unwrap_or(LiteralValue::Number(serial))
13565            }
13566            Some(FormatClass::Duration) => {
13567                let nanos = (serial * 86_400.0 * 1_000_000_000.0).round();
13568                if nanos.is_finite() && nanos >= i64::MIN as f64 && nanos <= i64::MAX as f64 {
13569                    LiteralValue::Duration(chrono::Duration::nanoseconds(nanos as i64))
13570                } else {
13571                    LiteralValue::Number(serial)
13572                }
13573            }
13574            _ => LiteralValue::Number(serial),
13575        }
13576    }
13577
13578    pub(crate) fn effective_format_id(
13579        &self,
13580        sheet: &str,
13581        row: u32,
13582        col: u32,
13583    ) -> Option<crate::format::FormatId> {
13584        let arrow = self.arrow_sheets.sheet(sheet).and_then(|arrow| {
13585            arrow.format_id(
13586                row.saturating_sub(1) as usize,
13587                col.saturating_sub(1) as usize,
13588            )
13589        });
13590        arrow.or_else(|| {
13591            let sheet_id = self.graph.sheet_id(sheet)?;
13592            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
13593            self.derived_formats.get(&cell)
13594        })
13595    }
13596
13597    /// Get a cell value through the single temporal egress boundary.
13598    pub fn get_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
13599        let raw = self.read_cell_value(sheet, row, col)?;
13600        let format = self.effective_format_id(sheet, row, col);
13601        let class = format.and_then(|id| self.format_registry.class(id));
13602        Self::normalize_public_cell_read(Self::materialize_temporal_egress(
13603            raw,
13604            class,
13605            self.config.temporal_egress,
13606            self.config.date_system,
13607        ))
13608    }
13609
13610    /// Read a rectangular range through the temporal egress boundary.
13611    pub fn get_range_values(
13612        &self,
13613        sheet: &str,
13614        sr: u32,
13615        sc: u32,
13616        er: u32,
13617        ec: u32,
13618    ) -> Vec<Vec<LiteralValue>> {
13619        let height = er.saturating_sub(sr).saturating_add(1) as usize;
13620        let width = ec.saturating_sub(sc).saturating_add(1) as usize;
13621        let Some(asheet) = self.sheet_store().sheet(sheet) else {
13622            return vec![vec![LiteralValue::Empty; width]; height];
13623        };
13624        let view = asheet.range_view(
13625            sr.saturating_sub(1) as usize,
13626            sc.saturating_sub(1) as usize,
13627            er.saturating_sub(1) as usize,
13628            ec.saturating_sub(1) as usize,
13629        );
13630        let sheet_id = self.graph.sheet_id(sheet);
13631        let derived_formats = &self.derived_formats;
13632        let has_derived_formats =
13633            sheet_id.is_some_and(|sheet_id| derived_formats.any(|cell| cell.sheet_id == sheet_id));
13634        let mut out = Vec::with_capacity(height);
13635        if !asheet.has_formats() && !has_derived_formats {
13636            for rr in 0..height {
13637                let mut row = Vec::with_capacity(width);
13638                for cc in 0..width {
13639                    row.push(view.get_cell(rr, cc));
13640                }
13641                out.push(row);
13642            }
13643            return out;
13644        }
13645        let format_registry = &self.format_registry;
13646        for rr in 0..height {
13647            let mut row = Vec::with_capacity(width);
13648            for cc in 0..width {
13649                let raw = view.get_cell(rr, cc);
13650                let row0 = sr.saturating_sub(1).saturating_add(rr as u32);
13651                let col0 = sc.saturating_sub(1).saturating_add(cc as u32);
13652                let format = asheet.format_id(row0 as usize, col0 as usize).or_else(|| {
13653                    let cell = CellRef::new(sheet_id?, Coord::new(row0, col0, true, true));
13654                    derived_formats.get(&cell)
13655                });
13656                let class = format.and_then(|id| format_registry.class(id));
13657                row.push(Self::materialize_temporal_egress(
13658                    raw,
13659                    class,
13660                    self.config.temporal_egress,
13661                    self.config.date_system,
13662                ));
13663            }
13664            out.push(row);
13665        }
13666        out
13667    }
13668
13669    /// Unified internal read API for a single cell value (Arrow-truth).
13670    /// The 1x1 spill fallback of a declared dynamic-array anchor: its
13671    /// committed result is a non-error, non-empty scalar. Scheduling orders
13672    /// the anchor before its spill readers (the reference keeps a static
13673    /// edge to it), exactly as for committed multi-cell spills.
13674    fn declared_anchor_holds_scalar(&self, anchor: CellRef) -> bool {
13675        let sheet = self.graph.sheet_name(anchor.sheet_id);
13676        if sheet.is_empty() {
13677            return false;
13678        }
13679        matches!(
13680            self.read_cell_value(sheet, anchor.coord.row() + 1, anchor.coord.col() + 1),
13681            Some(value) if !matches!(
13682                value,
13683                LiteralValue::Error(_)
13684                    | LiteralValue::Array(_)
13685                    | LiteralValue::Pending
13686                    | LiteralValue::Empty
13687            )
13688        )
13689    }
13690
13691    pub(crate) fn read_cell_value(&self, sheet: &str, row: u32, col: u32) -> Option<LiteralValue> {
13692        let asheet = self.sheet_store().sheet(sheet)?;
13693        let r0 = row.saturating_sub(1) as usize;
13694        let c0 = col.saturating_sub(1) as usize;
13695        let v = asheet.get_cell_value(r0, c0);
13696        if matches!(v, LiteralValue::Empty) {
13697            None
13698        } else {
13699            Some(v)
13700        }
13701    }
13702
13703    /// Unified internal read API for a range of cell values (Arrow-truth).
13704    pub(crate) fn read_range_values(
13705        &self,
13706        sheet: &str,
13707        sr: u32,
13708        sc: u32,
13709        er: u32,
13710        ec: u32,
13711    ) -> RangeView<'_> {
13712        let Some(asheet) = self.sheet_store().sheet(sheet) else {
13713            return RangeView::from_owned_rows(Vec::new(), self.config.date_system);
13714        };
13715        if er < sr || ec < sc {
13716            return asheet.range_view(1, 1, 0, 0);
13717        }
13718        let sr0 = sr.saturating_sub(1) as usize;
13719        let sc0 = sc.saturating_sub(1) as usize;
13720        let er0 = er.saturating_sub(1) as usize;
13721        let ec0 = ec.saturating_sub(1) as usize;
13722        asheet.range_view(sr0, sc0, er0, ec0)
13723    }
13724
13725    /// Get formula AST (if any) and current stored value for a cell
13726    pub fn get_cell(
13727        &self,
13728        sheet: &str,
13729        row: u32,
13730        col: u32,
13731    ) -> Option<(Option<formualizer_parse::ASTNode>, Option<LiteralValue>)> {
13732        let v = self.get_cell_value(sheet, row, col);
13733        let sheet_id = self.graph.sheet_id(sheet)?;
13734        let coord = Coord::from_excel(row, col, true, true);
13735        let cell = CellRef::new(sheet_id, coord);
13736        if let Some(vid) = self.graph.get_vertex_for_cell(&cell) {
13737            let ast = self.graph.get_formula(vid);
13738            Some((ast, v))
13739        } else if v.is_some() || self.graph.had_legacy_cell_vertex(&cell) {
13740            // A referenced or emptied value cell has no vertex (decision
13741            // 27), but it is a cell the graph knows, as it was when it had
13742            // one (the interactive formula edit routes on this).
13743            Some((None, v))
13744        } else {
13745            None
13746        }
13747    }
13748
13749    /// Begin batch operations - defer CSR rebuilds for better performance
13750    pub fn begin_batch(&mut self) {
13751        self.graph.begin_batch();
13752    }
13753
13754    /// End batch operations and trigger CSR rebuild
13755    pub fn end_batch(&mut self) {
13756        self.graph.end_batch();
13757    }
13758
13759    /// Begin a deferred-dirty scope for a multi-edit batch: while active,
13760    /// every edit's dirty propagation queues its sources instead of running
13761    /// a full BFS per edit, and the outermost `end_deferred_dirty` flushes
13762    /// the union with ONE multi-source propagation (O(component) instead of
13763    /// O(edits × component)). See `DependencyGraph::begin_deferred_dirty`.
13764    ///
13765    /// Callers MUST run `end_deferred_dirty` on every exit path, including
13766    /// error returns; evaluation entry points `debug_assert` no scope leaked.
13767    pub fn begin_deferred_dirty(&mut self) {
13768        self.graph.begin_deferred_dirty();
13769    }
13770
13771    /// End a deferred-dirty scope, flushing the queued propagation when the
13772    /// outermost scope closes. See `Engine::begin_deferred_dirty`.
13773    pub fn end_deferred_dirty(&mut self) {
13774        let _ = self.graph.end_deferred_dirty();
13775    }
13776
13777    /// Total vertices processed by dirty-propagation BFS loops since graph
13778    /// creation. Perf-shape observability only (cross-crate tests assert
13779    /// batched edits propagate O(component), not O(edits × component)).
13780    pub fn dirty_propagation_visits(&self) -> u64 {
13781        self.graph.dirty_propagation_visits()
13782    }
13783
13784    /// Evaluate a single vertex.
13785    /// This is the core of the sequential evaluation logic for Milestone 3.1.
13786    #[inline]
13787    fn record_cell_if_changed(
13788        delta: &mut DeltaCollector,
13789        cell: &CellRef,
13790        old: &LiteralValue,
13791        new: &LiteralValue,
13792    ) {
13793        if old != new {
13794            delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
13795        }
13796    }
13797
13798    pub fn evaluate_vertex(&mut self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
13799        self.observe_evaluation_resource_request(EvaluationRequestKind::Vertex, |engine| {
13800            engine.observe_function_semantic_epoch()?;
13801            // A direct request selects exactly one vertex, regardless of its formula kind.
13802            engine.resource_checkpoint(1)?;
13803            if !engine.graph.vertex_exists(vertex_id) {
13804                return engine.evaluate_vertex_impl(vertex_id, None);
13805            }
13806            let is_formula = matches!(
13807                engine.graph.get_vertex_kind(vertex_id),
13808                VertexKind::FormulaScalar | VertexKind::FormulaArray
13809            );
13810            if is_formula {
13811                engine.begin_evaluation_request();
13812                #[cfg(any(test, feature = "legacy_oracle"))]
13813                engine.graph.flush_pending_edge_deltas();
13814                let roots = [crate::engine::target_preparation::TargetProducer::Legacy(
13815                    vertex_id,
13816                )];
13817                engine.evaluate_legacy_target_roots(&roots, None)?;
13818            }
13819            engine.evaluate_vertex_impl(vertex_id, None)
13820        })
13821    }
13822
13823    /// Same rejection publication for owned arrays and pre-admitted range views.
13824    fn publish_oversized_spill(
13825        &mut self,
13826        vertex_id: VertexId,
13827        error: ExcelError,
13828        mut delta: Option<&mut DeltaCollector>,
13829    ) -> LiteralValue {
13830        self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
13831        let anchor = self
13832            .graph
13833            .get_cell_ref(vertex_id)
13834            .expect("cell ref for vertex");
13835        let spill_val = LiteralValue::Error(error);
13836        if let Some(d) = delta {
13837            let old = self
13838                .read_cell_value(
13839                    self.graph.sheet_name(anchor.sheet_id),
13840                    anchor.coord.row() + 1,
13841                    anchor.coord.col() + 1,
13842                )
13843                .unwrap_or(LiteralValue::Empty);
13844            if old != spill_val {
13845                d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
13846            }
13847        }
13848        self.graph.update_vertex_value_ref(vertex_id, &spill_val);
13849        if self.config.arrow_storage_enabled
13850            && self.config.delta_overlay_enabled
13851            && self.config.write_formula_overlay_enabled
13852        {
13853            let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
13854            self.mirror_value_to_computed_overlay(
13855                &sheet_name,
13856                anchor.coord.row() + 1,
13857                anchor.coord.col() + 1,
13858                &spill_val,
13859            );
13860        }
13861        spill_val
13862    }
13863
13864    fn evaluate_vertex_impl(
13865        &mut self,
13866        vertex_id: VertexId,
13867        delta: Option<&mut DeltaCollector>,
13868    ) -> Result<LiteralValue, ExcelError> {
13869        // Preserve the direct evaluator's compatibility behavior for invalid IDs, literal cells,
13870        // names, and other non-formula vertices. Only formula publication needs the C1a final
13871        // deadline checkpoint and effects pipeline.
13872        if !self.graph.vertex_exists(vertex_id) {
13873            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
13874                .with_message(format!("Vertex not found: {vertex_id:?}")));
13875        }
13876        if (self.active_resource_ledger.is_some()
13877            || self.graph.fixed_array_shapes.contains_key(&vertex_id))
13878            && matches!(
13879                self.graph.get_vertex_kind(vertex_id),
13880                VertexKind::FormulaScalar | VertexKind::FormulaArray
13881            )
13882        {
13883            let value = self
13884                .evaluate_vertex_immutable(vertex_id)
13885                .unwrap_or_else(LiteralValue::Error);
13886            let effects = self.plan_vertex_effects(vertex_id, value.clone())?;
13887            // Do not publish the selected result until the outer request's deadline succeeds.
13888            self.resource_checkpoint(0)?;
13889            let mut delta = delta;
13890            for effect in &effects {
13891                self.apply_effect_with_computed_writes(effect, delta.as_deref_mut(), None, None)?;
13892            }
13893            return Ok(value);
13894        }
13895
13896        let mut delta = delta;
13897
13898        // Get vertex kind and check if it needs evaluation
13899        let kind = self.graph.get_vertex_kind(vertex_id);
13900        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
13901
13902        let view = match kind {
13903            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
13904                if let Some(view) = self.graph.formula_view(vertex_id) {
13905                    view
13906                } else {
13907                    return Ok(LiteralValue::Number(0.0));
13908                }
13909            }
13910            VertexKind::Empty | VertexKind::Cell => {
13911                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
13912                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
13913                    let row = cell_ref.coord.row() + 1;
13914                    let col = cell_ref.coord.col() + 1;
13915                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
13916                        return Ok(v);
13917                    }
13918                }
13919                return Ok(LiteralValue::Number(0.0));
13920            }
13921            VertexKind::NamedScalar => {
13922                let value = self.evaluate_named_scalar(vertex_id, sheet_id)?;
13923                return Ok(value);
13924            }
13925            VertexKind::NamedArray => {
13926                let value = self.evaluate_named_array(vertex_id, sheet_id)?;
13927                return Ok(value);
13928            }
13929            VertexKind::InfiniteRange
13930            | VertexKind::Range
13931            | VertexKind::External
13932            | VertexKind::Table => {
13933                // Not directly evaluatable here.
13934                return Ok(LiteralValue::Number(0.0));
13935            }
13936        };
13937
13938        // The interpreter uses a reference to the engine as the context.
13939        let sheet_name = self.graph.sheet_name(sheet_id);
13940        let cell_ref = self
13941            .graph
13942            .get_cell_ref(vertex_id)
13943            .expect("cell ref for vertex");
13944        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
13945
13946        let result = interpreter.evaluate_formula_view(
13947            view,
13948            self.graph.data_store(),
13949            self.graph.sheet_reg(),
13950        );
13951
13952        // If array result, perform spill from the anchor cell
13953        match result {
13954            Ok(cv) => {
13955                let derived_format = cv.format_id();
13956                self.record_derived_format(vertex_id, derived_format);
13957                let oversized_range = if matches!(cv, crate::traits::CalcValue::Range(_))
13958                    && self.graph.fixed_single_arrays.contains(&vertex_id)
13959                {
13960                    None
13961                } else {
13962                    crate::engine::result_finalization::range_spill_error(
13963                        &cv,
13964                        self.config.spill.max_spill_cells,
13965                    )
13966                };
13967                let is_oversized_range = oversized_range.is_some();
13968                let result_literal = if let Some(error) = oversized_range {
13969                    drop(cv);
13970                    LiteralValue::Error(error)
13971                } else {
13972                    self.materialize_formula_result(vertex_id, cv)
13973                };
13974                let output_sheet_name = sheet_name.to_string();
13975                self.write_computed_overlay_format_0based(
13976                    &output_sheet_name,
13977                    cell_ref.coord.row(),
13978                    cell_ref.coord.col(),
13979                    derived_format,
13980                );
13981                if is_oversized_range {
13982                    let LiteralValue::Error(error) = result_literal else {
13983                        unreachable!()
13984                    };
13985                    self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
13986                    return Ok(self.publish_oversized_spill(
13987                        vertex_id,
13988                        error,
13989                        delta.as_deref_mut(),
13990                    ));
13991                }
13992                match result_literal {
13993                    LiteralValue::Array(rows) => {
13994                        // Update kind to FormulaArray for tracking
13995                        self.graph
13996                            .set_kind(vertex_id, crate::engine::vertex::VertexKind::FormulaArray);
13997                        // Build target cells rectangle starting from anchor
13998                        let anchor = self
13999                            .graph
14000                            .get_cell_ref(vertex_id)
14001                            .expect("cell ref for vertex");
14002                        let sheet_id = anchor.sheet_id;
14003                        let h = rows.len() as u32;
14004                        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
14005
14006                        // Hard cap to avoid vertex explosion from huge dynamic arrays.
14007                        let spill_cells = (h as u64).saturating_mul(w as u64);
14008                        if spill_cells > self.config.spill.max_spill_cells as u64 {
14009                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
14010                                .with_message("SpillTooLarge")
14011                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
14012                                    expected_rows: h,
14013                                    expected_cols: w,
14014                                });
14015                            return Ok(self.publish_oversized_spill(
14016                                vertex_id,
14017                                spill_err,
14018                                delta.as_deref_mut(),
14019                            ));
14020                        }
14021                        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity)
14022                        const PACKED_MAX_ROW: u32 = 1_048_575; // 20-bit max
14023                        const PACKED_MAX_COL: u32 = 16_383; // 14-bit max
14024                        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
14025                        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
14026                        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
14027                            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
14028                            let spill_err = ExcelError::new(ExcelErrorKind::Spill)
14029                                .with_message("Spill exceeds sheet bounds")
14030                                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
14031                                    expected_rows: h,
14032                                    expected_cols: w,
14033                                });
14034                            let spill_val = LiteralValue::Error(spill_err.clone());
14035                            if let Some(d) = delta.as_deref_mut() {
14036                                let old = self
14037                                    .read_cell_value(
14038                                        self.graph.sheet_name(anchor.sheet_id),
14039                                        anchor.coord.row() + 1,
14040                                        anchor.coord.col() + 1,
14041                                    )
14042                                    .unwrap_or(LiteralValue::Empty);
14043                                if old != spill_val {
14044                                    d.record_cell(
14045                                        anchor.sheet_id,
14046                                        anchor.coord.row(),
14047                                        anchor.coord.col(),
14048                                    );
14049                                }
14050                            }
14051                            self.graph.update_vertex_value_ref(vertex_id, &spill_val);
14052                            if self.config.arrow_storage_enabled
14053                                && self.config.delta_overlay_enabled
14054                                && self.config.write_formula_overlay_enabled
14055                            {
14056                                let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
14057                                self.mirror_value_to_computed_overlay(
14058                                    &sheet_name,
14059                                    anchor.coord.row() + 1,
14060                                    anchor.coord.col() + 1,
14061                                    &spill_val,
14062                                );
14063                            }
14064                            return Ok(spill_val);
14065                        }
14066                        let mut targets = Vec::new();
14067                        for r in 0..h {
14068                            for c in 0..w {
14069                                targets.push(self.graph.make_cell_ref_internal(
14070                                    sheet_id,
14071                                    anchor.coord.row() + r,
14072                                    anchor.coord.col() + c,
14073                                ));
14074                            }
14075                        }
14076
14077                        // Plan spill via spill manager shim
14078                        match self.spill_mgr.reserve(
14079                            vertex_id,
14080                            anchor,
14081                            SpillShape { rows: h, cols: w },
14082                            SpillMeta {
14083                                epoch: self.recalc_epoch,
14084                                config: self.config.spill,
14085                            },
14086                        ) {
14087                            Ok(()) => {
14088                                // Commit: write values to grid
14089                                // Default conflict policy is Error + FirstWins; reserve() enforces in-flight locks
14090                                // and plan_spill_region enforces overlap with committed formulas/spills/values.
14091                                if let Err(e) = self.commit_spill_and_mirror(
14092                                    vertex_id,
14093                                    &targets,
14094                                    rows.clone(),
14095                                    delta.as_deref_mut(),
14096                                ) {
14097                                    if e.kind != ExcelErrorKind::Spill {
14098                                        return Err(e);
14099                                    }
14100                                    // If commit fails, mark as error
14101                                    self.clear_spill_projection_and_mirror(
14102                                        vertex_id,
14103                                        delta.as_deref_mut(),
14104                                    );
14105                                    if let Some(d) = delta.as_deref_mut() {
14106                                        let old = self
14107                                            .read_cell_value(
14108                                                self.graph.sheet_name(anchor.sheet_id),
14109                                                anchor.coord.row() + 1,
14110                                                anchor.coord.col() + 1,
14111                                            )
14112                                            .unwrap_or(LiteralValue::Empty);
14113                                        let new = LiteralValue::Error(e.clone());
14114                                        if old != new {
14115                                            d.record_cell(
14116                                                anchor.sheet_id,
14117                                                anchor.coord.row(),
14118                                                anchor.coord.col(),
14119                                            );
14120                                        }
14121                                    }
14122                                    let err_val = LiteralValue::Error(e.clone());
14123                                    self.graph.update_vertex_value_ref(vertex_id, &err_val);
14124                                    if self.config.arrow_storage_enabled
14125                                        && self.config.delta_overlay_enabled
14126                                        && self.config.write_formula_overlay_enabled
14127                                    {
14128                                        let sheet_name =
14129                                            self.graph.sheet_name(anchor.sheet_id).to_string();
14130                                        self.mirror_value_to_computed_overlay(
14131                                            &sheet_name,
14132                                            anchor.coord.row() + 1,
14133                                            anchor.coord.col() + 1,
14134                                            &err_val,
14135                                        );
14136                                    }
14137                                    return Ok(err_val);
14138                                }
14139                                // Anchor shows the top-left value, like Excel
14140                                let top_left = rows
14141                                    .first()
14142                                    .and_then(|r| r.first())
14143                                    .cloned()
14144                                    .unwrap_or(LiteralValue::Empty);
14145                                self.graph.update_vertex_value_ref(vertex_id, &top_left);
14146                                Ok(top_left)
14147                            }
14148                            Err(e) => {
14149                                self.clear_spill_projection_and_mirror(
14150                                    vertex_id,
14151                                    delta.as_deref_mut(),
14152                                );
14153                                let spill_err = ExcelError::new(ExcelErrorKind::Spill)
14154                                    .with_message(
14155                                        e.message.unwrap_or_else(|| "Spill blocked".to_string()),
14156                                    )
14157                                    .with_extra(formualizer_common::ExcelErrorExtra::Spill {
14158                                        expected_rows: h,
14159                                        expected_cols: w,
14160                                    });
14161                                let spill_val = LiteralValue::Error(spill_err.clone());
14162                                if let Some(d) = delta.as_deref_mut() {
14163                                    let old = self
14164                                        .read_cell_value(
14165                                            self.graph.sheet_name(anchor.sheet_id),
14166                                            anchor.coord.row() + 1,
14167                                            anchor.coord.col() + 1,
14168                                        )
14169                                        .unwrap_or(LiteralValue::Empty);
14170                                    if old != spill_val {
14171                                        d.record_cell(
14172                                            anchor.sheet_id,
14173                                            anchor.coord.row(),
14174                                            anchor.coord.col(),
14175                                        );
14176                                    }
14177                                }
14178                                self.graph.update_vertex_value_ref(vertex_id, &spill_val);
14179                                if self.config.arrow_storage_enabled
14180                                    && self.config.delta_overlay_enabled
14181                                    && self.config.write_formula_overlay_enabled
14182                                {
14183                                    let sheet_name =
14184                                        self.graph.sheet_name(anchor.sheet_id).to_string();
14185                                    self.mirror_value_to_computed_overlay(
14186                                        &sheet_name,
14187                                        anchor.coord.row() + 1,
14188                                        anchor.coord.col() + 1,
14189                                        &spill_val,
14190                                    );
14191                                }
14192                                Ok(spill_val)
14193                            }
14194                        }
14195                    }
14196                    other => {
14197                        // Scalar result: store value and ensure any previous spill is cleared
14198                        let spill_cells = self.graph.spill_cells_to_clear(vertex_id);
14199                        if let Some(d) = delta.as_deref_mut()
14200                            && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
14201                        {
14202                            if spill_cells.is_empty() {
14203                                let old = self
14204                                    .read_cell_value(
14205                                        self.graph.sheet_name(anchor.sheet_id),
14206                                        anchor.coord.row() + 1,
14207                                        anchor.coord.col() + 1,
14208                                    )
14209                                    .unwrap_or(LiteralValue::Empty);
14210                                if old != other {
14211                                    d.record_cell(
14212                                        anchor.sheet_id,
14213                                        anchor.coord.row(),
14214                                        anchor.coord.col(),
14215                                    );
14216                                }
14217                            } else {
14218                                for cell in spill_cells.iter() {
14219                                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
14220                                    let old = self
14221                                        .get_cell_value(
14222                                            sheet_name,
14223                                            cell.coord.row() + 1,
14224                                            cell.coord.col() + 1,
14225                                        )
14226                                        .unwrap_or(LiteralValue::Empty);
14227                                    let new = if cell.sheet_id == anchor.sheet_id
14228                                        && cell.coord.row() == anchor.coord.row()
14229                                        && cell.coord.col() == anchor.coord.col()
14230                                    {
14231                                        other.clone()
14232                                    } else {
14233                                        LiteralValue::Empty
14234                                    };
14235                                    Self::record_cell_if_changed(d, cell, &old, &new);
14236                                }
14237                            }
14238                        }
14239                        self.graph.clear_spill_region(vertex_id);
14240                        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
14241                            self.record_structural_change(scope);
14242                        }
14243                        if self.config.arrow_storage_enabled
14244                            && self.config.delta_overlay_enabled
14245                            && self.config.write_formula_overlay_enabled
14246                        {
14247                            let empty = LiteralValue::Empty;
14248                            for cell in spill_cells.iter() {
14249                                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
14250                                self.mirror_value_to_computed_overlay(
14251                                    &sheet_name,
14252                                    cell.coord.row() + 1,
14253                                    cell.coord.col() + 1,
14254                                    &empty,
14255                                );
14256                            }
14257                        }
14258                        self.graph.update_vertex_value_ref(vertex_id, &other);
14259                        // Optionally mirror into Arrow overlay for Arrow-backed reads
14260                        if self.config.arrow_storage_enabled
14261                            && self.config.delta_overlay_enabled
14262                            && self.config.write_formula_overlay_enabled
14263                        {
14264                            let anchor = self
14265                                .graph
14266                                .get_cell_ref(vertex_id)
14267                                .expect("cell ref for vertex");
14268                            let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
14269                            self.mirror_value_to_computed_overlay(
14270                                &sheet_name,
14271                                anchor.coord.row() + 1,
14272                                anchor.coord.col() + 1,
14273                                &other,
14274                            );
14275                        }
14276                        Ok(other)
14277                    }
14278                }
14279            }
14280            Err(e) => {
14281                // Runtime Excel error: store as a cell value instead of propagating
14282                // as an exception so bulk eval paths don't fail the whole pass.
14283                let spill_cells = self.graph.spill_cells_to_clear(vertex_id);
14284                let err_val = LiteralValue::Error(e.clone());
14285                if let Some(d) = delta
14286                    && let Some(anchor) = self.graph.get_cell_ref_for_vertex(vertex_id)
14287                {
14288                    if spill_cells.is_empty() {
14289                        let old = self
14290                            .read_cell_value(
14291                                self.graph.sheet_name(anchor.sheet_id),
14292                                anchor.coord.row() + 1,
14293                                anchor.coord.col() + 1,
14294                            )
14295                            .unwrap_or(LiteralValue::Empty);
14296                        if old != err_val {
14297                            d.record_cell(anchor.sheet_id, anchor.coord.row(), anchor.coord.col());
14298                        }
14299                    } else {
14300                        for cell in spill_cells.iter() {
14301                            let sheet_name = self.graph.sheet_name(cell.sheet_id);
14302                            let old = self
14303                                .get_cell_value(
14304                                    sheet_name,
14305                                    cell.coord.row() + 1,
14306                                    cell.coord.col() + 1,
14307                                )
14308                                .unwrap_or(LiteralValue::Empty);
14309                            let new = if cell.sheet_id == anchor.sheet_id
14310                                && cell.coord.row() == anchor.coord.row()
14311                                && cell.coord.col() == anchor.coord.col()
14312                            {
14313                                err_val.clone()
14314                            } else {
14315                                LiteralValue::Empty
14316                            };
14317                            Self::record_cell_if_changed(d, cell, &old, &new);
14318                        }
14319                    }
14320                }
14321                self.graph.clear_spill_region(vertex_id);
14322                if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
14323                    self.record_structural_change(scope);
14324                }
14325                if self.config.arrow_storage_enabled
14326                    && self.config.delta_overlay_enabled
14327                    && self.config.write_formula_overlay_enabled
14328                {
14329                    let empty = LiteralValue::Empty;
14330                    for cell in spill_cells.iter() {
14331                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
14332                        self.mirror_value_to_computed_overlay(
14333                            &sheet_name,
14334                            cell.coord.row() + 1,
14335                            cell.coord.col() + 1,
14336                            &empty,
14337                        );
14338                    }
14339                }
14340                self.graph.update_vertex_value_ref(vertex_id, &err_val);
14341                if self.config.arrow_storage_enabled
14342                    && self.config.delta_overlay_enabled
14343                    && self.config.write_formula_overlay_enabled
14344                {
14345                    let anchor = self
14346                        .graph
14347                        .get_cell_ref(vertex_id)
14348                        .expect("cell ref for vertex");
14349                    let sheet_name = self.graph.sheet_name(anchor.sheet_id).to_string();
14350                    self.mirror_value_to_computed_overlay(
14351                        &sheet_name,
14352                        anchor.coord.row() + 1,
14353                        anchor.coord.col() + 1,
14354                        &err_val,
14355                    );
14356                }
14357                Ok(err_val)
14358            }
14359        }
14360    }
14361
14362    fn evaluate_named_scalar(
14363        &mut self,
14364        vertex_id: VertexId,
14365        sheet_id: SheetId,
14366    ) -> Result<LiteralValue, ExcelError> {
14367        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
14368            ExcelError::new(ExcelErrorKind::Name)
14369                .with_message("Named range metadata missing".to_string())
14370        })?;
14371
14372        match &named_range.definition {
14373            NamedDefinition::Cell(cell_ref) => {
14374                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
14375                let row = cell_ref.coord.row() + 1;
14376                let col = cell_ref.coord.col() + 1;
14377
14378                if let Some(dep_vertex) = self.graph.get_vertex_for_cell(cell_ref)
14379                    && matches!(
14380                        self.graph.get_vertex_kind(dep_vertex),
14381                        VertexKind::FormulaScalar | VertexKind::FormulaArray
14382                    )
14383                {
14384                    // Graph does not cache cell/formula values; ensure the precedent is evaluated.
14385                    let value = self.evaluate_vertex(dep_vertex)?;
14386                    self.graph.update_vertex_value_ref(vertex_id, &value);
14387                    Ok(value)
14388                } else {
14389                    let value = self
14390                        .get_cell_value(sheet_name, row, col)
14391                        .unwrap_or(LiteralValue::Empty);
14392                    self.graph.update_vertex_value_ref(vertex_id, &value);
14393                    Ok(value)
14394                }
14395            }
14396            NamedDefinition::Literal(v) => {
14397                let out = v.clone();
14398                self.graph.update_vertex_value_ref(vertex_id, &out);
14399                Ok(out)
14400            }
14401            NamedDefinition::Formula { ast, .. } => {
14402                let context_sheet = match named_range.scope {
14403                    NameScope::Sheet(id) => id,
14404                    NameScope::Workbook => sheet_id,
14405                };
14406                let sheet_name = self.graph.sheet_name(context_sheet);
14407                let cell_ref = self
14408                    .graph
14409                    .get_cell_ref(vertex_id)
14410                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
14411                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
14412                match interpreter.evaluate_ast(ast) {
14413                    Ok(cv) => {
14414                        let value = cv.into_literal();
14415                        match value {
14416                            LiteralValue::Array(_) => {
14417                                let err = ExcelError::new(ExcelErrorKind::NImpl)
14418                                    .with_message("Array result in scalar named range".to_string());
14419                                let err_val = LiteralValue::Error(err.clone());
14420                                self.graph.update_vertex_value_ref(vertex_id, &err_val);
14421                                Ok(err_val)
14422                            }
14423                            other => {
14424                                self.graph.update_vertex_value_ref(vertex_id, &other);
14425                                Ok(other)
14426                            }
14427                        }
14428                    }
14429                    Err(err) => {
14430                        let err_val = LiteralValue::Error(err.clone());
14431                        self.graph.update_vertex_value_ref(vertex_id, &err_val);
14432                        Ok(err_val)
14433                    }
14434                }
14435            }
14436            NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
14437                .with_message("Range-valued name evaluated as scalar".to_string())),
14438        }
14439    }
14440
14441    fn evaluate_named_array(
14442        &mut self,
14443        vertex_id: VertexId,
14444        sheet_id: SheetId,
14445    ) -> Result<LiteralValue, ExcelError> {
14446        let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
14447            ExcelError::new(ExcelErrorKind::Name)
14448                .with_message("Named range metadata missing".to_string())
14449        })?;
14450
14451        let out = match &named_range.definition {
14452            NamedDefinition::Range(range_ref) => {
14453                if range_ref.start.sheet_id != range_ref.end.sheet_id {
14454                    return Err(ExcelError::new(ExcelErrorKind::Ref)
14455                        .with_message("Named range cannot span sheets".to_string()));
14456                }
14457
14458                let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
14459                let sr0 = range_ref.start.coord.row();
14460                let sc0 = range_ref.start.coord.col();
14461                let er0 = range_ref.end.coord.row();
14462                let ec0 = range_ref.end.coord.col();
14463                if sr0 > er0 || sc0 > ec0 {
14464                    return Err(ExcelError::new(ExcelErrorKind::Ref)
14465                        .with_message("Invalid named range bounds".to_string()));
14466                }
14467
14468                let h = (er0 - sr0 + 1) as usize;
14469                let w = (ec0 - sc0 + 1) as usize;
14470                let cell_count = (h as u64).saturating_mul(w as u64);
14471                if cell_count > self.config.spill.max_spill_cells as u64 {
14472                    return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
14473                        "Named range too large to materialize as an array".to_string(),
14474                    ));
14475                }
14476
14477                let mut rows = Vec::with_capacity(h);
14478                for r0 in sr0..=er0 {
14479                    let mut row = Vec::with_capacity(w);
14480                    for c0 in sc0..=ec0 {
14481                        let v = self
14482                            .get_cell_value(sheet_name, r0 + 1, c0 + 1)
14483                            .unwrap_or(LiteralValue::Empty);
14484                        row.push(v);
14485                    }
14486                    rows.push(row);
14487                }
14488                LiteralValue::Array(rows)
14489            }
14490            NamedDefinition::Cell(cell_ref) => {
14491                let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
14492                let row = cell_ref.coord.row() + 1;
14493                let col = cell_ref.coord.col() + 1;
14494                let v = self
14495                    .get_cell_value(sheet_name, row, col)
14496                    .unwrap_or(LiteralValue::Empty);
14497                LiteralValue::Array(vec![vec![v]])
14498            }
14499            NamedDefinition::Literal(v) => LiteralValue::Array(vec![vec![v.clone()]]),
14500            NamedDefinition::Formula { ast, .. } => {
14501                let context_sheet = match named_range.scope {
14502                    NameScope::Sheet(id) => id,
14503                    NameScope::Workbook => sheet_id,
14504                };
14505                let sheet_name = self.graph.sheet_name(context_sheet);
14506                let cell_ref = self
14507                    .graph
14508                    .get_cell_ref(vertex_id)
14509                    .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
14510                let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
14511                match interpreter.evaluate_ast(ast) {
14512                    Ok(cv) => {
14513                        let v = cv.into_literal();
14514                        match v {
14515                            LiteralValue::Array(_) => v,
14516                            other => LiteralValue::Array(vec![vec![other]]),
14517                        }
14518                    }
14519                    Err(err) => LiteralValue::Error(err),
14520                }
14521            }
14522        };
14523
14524        self.graph.update_vertex_value_ref(vertex_id, &out);
14525        Ok(out)
14526    }
14527
14528    fn replan_exhausted_error(&self, limit: usize, context: &str) -> ExcelError {
14529        crate::engine::ResourceLedgerError::Exhausted(
14530            formualizer_common::ResourceExhaustionDetail {
14531                reason: formualizer_common::ResourceExhaustionReason::WorkUnits,
14532                limit: limit as u64,
14533                observed: limit.saturating_add(1) as u64,
14534                request_id: self
14535                    .active_evaluation_resource_request
14536                    .as_ref()
14537                    .map(|stats| stats.request_id),
14538            },
14539        )
14540        .into_excel_error()
14541        .with_message(format!("{context} did not converge after {limit} replans"))
14542    }
14543
14544    fn transient_target_preparation_stale(error: &ExcelError) -> bool {
14545        matches!(
14546            &error.extra,
14547            formualizer_common::ExcelErrorExtra::PreparationStale {
14548                reason: formualizer_common::PreparationStaleReason::Semantic
14549                    | formualizer_common::PreparationStaleReason::Provider
14550            }
14551        )
14552    }
14553
14554    fn prepare_graph_for_routed_evaluation(
14555        &mut self,
14556        targets: &[crate::engine::EvaluationTarget],
14557        options: &crate::engine::TargetEvalOptions<'_>,
14558    ) -> Result<crate::engine::PreparedTargetGraphReport, ExcelError> {
14559        const MAX_TRANSIENT_PREPARATION_RETRIES: usize = 2;
14560        let mut retries = 0usize;
14561        loop {
14562            match self.prepare_graph_for_targets_unobserved(targets, options) {
14563                Err(error)
14564                    if Self::transient_target_preparation_stale(&error)
14565                        && retries < MAX_TRANSIENT_PREPARATION_RETRIES =>
14566                {
14567                    retries = retries.saturating_add(1);
14568                }
14569                result => return result,
14570            }
14571        }
14572    }
14573
14574    fn evaluate_mixed_targets(
14575        &mut self,
14576        targets: &[crate::engine::EvaluationTarget],
14577        delta: Option<&mut DeltaCollector>,
14578    ) -> Result<EvalResult, ExcelError> {
14579        let _source_cache = self.source_cache_session();
14580        let cancel = self.active_cancel_flag.clone();
14581        let options = crate::engine::TargetEvalOptions {
14582            request_id: self
14583                .active_evaluation_resource_request
14584                .as_ref()
14585                .map(|stats| stats.request_id),
14586            cancel,
14587            deadline: None,
14588            budgets: None,
14589            opaque_policy: crate::engine::OpaquePreparePolicy::Widen,
14590        };
14591        self.prepare_and_execute_target_recipe(targets, &options, delta)
14592    }
14593
14594    fn prepare_and_execute_target_recipe(
14595        &mut self,
14596        targets: &[crate::engine::EvaluationTarget],
14597        options: &crate::engine::TargetEvalOptions<'_>,
14598        delta: Option<&mut DeltaCollector>,
14599    ) -> Result<EvalResult, ExcelError> {
14600        let preparation = self.prepare_graph_for_routed_evaluation(targets, options)?;
14601        self.execute_prepared_target_recipe(targets, &preparation.widened_scope, delta)
14602    }
14603
14604    fn execute_prepared_target_recipe(
14605        &mut self,
14606        targets: &[crate::engine::EvaluationTarget],
14607        scope: &crate::engine::PrepareScope,
14608        delta: Option<&mut DeltaCollector>,
14609    ) -> Result<EvalResult, ExcelError> {
14610        self.require_unified_authority()?;
14611        if matches!(scope, crate::engine::PrepareScope::Workbook)
14612            && let Some(stats) = self.active_evaluation_resource_request.as_mut()
14613        {
14614            stats.workbook_exact_attempts = stats.workbook_exact_attempts.max(1);
14615        }
14616        let mut roots = self.resolve_target_producers(targets)?;
14617        if let crate::engine::PrepareScope::Sheets(sheets) = scope {
14618            let request_id = self
14619                .active_evaluation_resource_request
14620                .as_ref()
14621                .map(|request| request.request_id);
14622            let root_count = roots.len();
14623            let mut widened_roots =
14624                OrderedTargetProducers::from_ordered(std::mem::take(&mut roots))
14625                    .map_err(|_| target_root_allocation_error(root_count, request_id))?;
14626            let sheet_ids = sheets
14627                .iter()
14628                .filter_map(|sheet| self.graph.sheet_id(sheet))
14629                .collect::<FxHashSet<_>>();
14630            for vertex in self.graph.formula_vertices() {
14631                if sheet_ids.contains(&self.graph.get_vertex_sheet_id(vertex)) {
14632                    widened_roots
14633                        .push(crate::engine::target_preparation::TargetProducer::Legacy(
14634                            vertex,
14635                        ))
14636                        .map_err(|_| {
14637                            target_root_allocation_error(widened_roots.len() + 1, request_id)
14638                        })?;
14639                }
14640            }
14641            roots = widened_roots.into_vec();
14642        }
14643        self.begin_evaluation_request();
14644        #[cfg(any(test, feature = "legacy_oracle"))]
14645        self.graph.flush_pending_edge_deltas();
14646        let workbook_scope = matches!(scope, crate::engine::PrepareScope::Workbook);
14647        if workbook_scope {
14648            if let Some(delta) = delta {
14649                self.evaluate_all_with_delta_collector(delta)
14650            } else {
14651                self.evaluate_all_legacy_impl()
14652            }
14653        } else {
14654            self.evaluate_legacy_target_roots(&roots, delta)
14655        }
14656    }
14657
14658    fn legacy_coordinate_targets(
14659        &mut self,
14660        targets: &[(&str, u32, u32)],
14661    ) -> Vec<crate::engine::EvaluationTarget> {
14662        targets
14663            .iter()
14664            .map(|(sheet, row, col)| {
14665                // Compatibility APIs historically interned an unknown target sheet
14666                // and returned an empty value rather than rejecting the target.
14667                self.graph.sheet_id_mut(sheet);
14668                crate::engine::EvaluationTarget::Cell {
14669                    sheet: (*sheet).to_string(),
14670                    row: *row,
14671                    col: *col,
14672                }
14673            })
14674            .collect()
14675    }
14676
14677    /// Evaluate the necessary mixed producer closure for typed cell, range, name, and table targets.
14678    pub fn evaluate_targets(
14679        &mut self,
14680        targets: &[crate::engine::EvaluationTarget],
14681    ) -> Result<EvalResult, ExcelError> {
14682        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
14683            engine.observe_function_semantic_epoch()?;
14684            engine.validate_deterministic_mode()?;
14685            engine.evaluate_mixed_targets(targets, None)
14686        })
14687    }
14688
14689    /// Evaluate typed targets with explicit preparation policy and request controls.
14690    pub fn evaluate_targets_with_options(
14691        &mut self,
14692        targets: &[crate::engine::EvaluationTarget],
14693        options: crate::engine::TargetEvalOptions<'_>,
14694    ) -> Result<EvalResult, ExcelError> {
14695        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
14696            engine.active_cancel_flag = options.cancel.clone();
14697            engine.active_evaluation_deadline = options.deadline;
14698            let result = (|| {
14699                engine.cancellation_checkpoint("Evaluation cancelled before target preparation")?;
14700                engine.observe_function_semantic_epoch()?;
14701                engine.validate_deterministic_mode()?;
14702                let _source_cache = engine.source_cache_session();
14703                engine.prepare_and_execute_target_recipe(targets, &options, None)
14704            })();
14705            engine.active_cancel_flag = None;
14706            engine.active_evaluation_deadline = None;
14707            result
14708        })
14709    }
14710
14711    /// Evaluate typed targets and return the versioned run/region delta for the request.
14712    pub fn evaluate_targets_with_delta(
14713        &mut self,
14714        targets: &[crate::engine::EvaluationTarget],
14715    ) -> Result<(EvalResult, crate::engine::TargetEvalDelta), ExcelError> {
14716        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
14717            engine.observe_function_semantic_epoch()?;
14718            engine.validate_deterministic_mode()?;
14719            let mut collector = DeltaCollector::new(DeltaMode::Cells);
14720            let result = engine.evaluate_mixed_targets(targets, Some(&mut collector))?;
14721            Ok((result, collector.finish_target()))
14722        })
14723    }
14724
14725    /// Evaluate only the necessary precedents for specific target cells (demand-driven)
14726    pub fn evaluate_until(
14727        &mut self,
14728        targets: &[(&str, u32, u32)],
14729    ) -> Result<EvalResult, ExcelError> {
14730        self.observe_evaluation_resource_request(EvaluationRequestKind::Targeted, |engine| {
14731            engine.evaluate_until_unobserved(targets)
14732        })
14733    }
14734
14735    fn evaluate_until_unobserved(
14736        &mut self,
14737        targets: &[(&str, u32, u32)],
14738    ) -> Result<EvalResult, ExcelError> {
14739        self.observe_function_semantic_epoch()?;
14740        let targets = self.legacy_coordinate_targets(targets);
14741        self.evaluate_mixed_targets(&targets, None)
14742    }
14743
14744    fn evaluate_until_with_delta_collector(
14745        &mut self,
14746        targets: &[(&str, u32, u32)],
14747        delta: &mut DeltaCollector,
14748    ) -> Result<EvalResult, ExcelError> {
14749        let targets = self.legacy_coordinate_targets(targets);
14750        self.evaluate_mixed_targets(&targets, Some(delta))
14751    }
14752
14753    fn evaluate_legacy_target_roots(
14754        &mut self,
14755        roots: &[crate::engine::target_preparation::TargetProducer],
14756        mut delta: Option<&mut DeltaCollector>,
14757    ) -> Result<EvalResult, ExcelError> {
14758        use crate::engine::target_preparation::TargetProducer;
14759        #[cfg(any(test, feature = "benchmark_internal"))]
14760        {
14761            self.recalc_reuse_probe
14762                .get_mut()
14763                .unwrap()
14764                .legacy_target_requests += 1;
14765        }
14766        let start = crate::instant::FzInstant::now();
14767        let root_vertices = roots
14768            .iter()
14769            .filter_map(|root| match root {
14770                TargetProducer::Legacy(vertex) | TargetProducer::Symbol(vertex) => Some(*vertex),
14771                TargetProducer::ValueOnly(_) => None,
14772            })
14773            .collect::<Vec<_>>();
14774        let mut computed_vertices = 0usize;
14775        let mut cycle_errors = 0usize;
14776        let mut replans = 0usize;
14777        const MAX_REPLAN: usize = 5;
14778        self.graph.authority_sync();
14779        loop {
14780            let (precedents_to_eval, old_vdeps) = self.demand_subgraph(&root_vertices)?;
14781            if precedents_to_eval.is_empty() {
14782                break;
14783            }
14784            #[cfg(any(test, feature = "benchmark_internal"))]
14785            {
14786                self.recalc_reuse_probe
14787                    .get_mut()
14788                    .unwrap()
14789                    .target_schedule_builds += 1;
14790            }
14791            let schedule = {
14792                self.graph.authority_sync();
14793                let mut ledger = self.active_resource_ledger.take();
14794                let result = self.create_authority_schedule(
14795                    &precedents_to_eval,
14796                    &old_vdeps,
14797                    ledger.as_mut(),
14798                );
14799                self.active_resource_ledger = ledger;
14800                result?
14801            };
14802            self.begin_pass(&schedule);
14803            for (unit_index, &unit) in schedule.units.iter().enumerate() {
14804                self.cancellation_checkpoint("Evaluation cancelled before target schedule unit")?;
14805                match unit {
14806                    ScheduleUnit::Cycle(index) => {
14807                        if self.handle_cycle_unit(
14808                            schedule.unit_cycle(index),
14809                            delta.as_deref_mut(),
14810                            None,
14811                            None,
14812                        )? > 0
14813                        {
14814                            cycle_errors = cycle_errors.saturating_add(1);
14815                        }
14816                    }
14817                    ScheduleUnit::Layer(index) => {
14818                        let layer = schedule.unit_layer(index);
14819                        let evaluated = if let Some(delta) = delta.as_deref_mut() {
14820                            if self.thread_pool.is_some() && layer.vertices.len() > 1 {
14821                                self.evaluate_layer_parallel_with_delta(layer, delta)?
14822                            } else {
14823                                self.evaluate_layer_sequential_with_delta(layer, delta)?
14824                            }
14825                        } else if self.thread_pool.is_some() && layer.vertices.len() > 1 {
14826                            self.evaluate_layer_parallel(layer)?
14827                        } else {
14828                            self.evaluate_layer_sequential(layer)?
14829                        };
14830                        computed_vertices = computed_vertices.saturating_add(evaluated);
14831                    }
14832                }
14833                if self.stop_after_unit(&schedule, unit_index) {
14834                    break;
14835                }
14836            }
14837            let changed = self.changed_virtual_dep_vertices(&precedents_to_eval, &old_vdeps);
14838            self.resource_checkpoint(0)?;
14839            if !self.finish_target_pass_dirty(&precedents_to_eval, &changed) {
14840                break;
14841            }
14842            if replans >= MAX_REPLAN {
14843                return Err(self.replan_exhausted_error(
14844                    MAX_REPLAN,
14845                    "targeted legacy dynamic dependency evaluation",
14846                ));
14847            }
14848            replans = replans.saturating_add(1);
14849        }
14850        self.redirty_for_next_recalc();
14851        Ok(EvalResult {
14852            computed_vertices,
14853            cycle_errors,
14854            elapsed: start.elapsed(),
14855        })
14856    }
14857
14858    /// Build a revision-bound compatibility plan covering every prepared formula vertex.
14859    pub fn build_recalc_plan(&self) -> Result<RecalcPlan, ExcelError> {
14860        if self.has_staged_formulas() || self.staged_formula_index.has_packages() {
14861            return Err(
14862                Self::plan_stale(formualizer_common::PlanStaleReason::Staged).with_message(
14863                    "compatibility recalculation plans require all staged formulas to be prepared",
14864                ),
14865            );
14866        }
14867        let key = self.recalc_plan_key();
14868        let mut vertices: Vec<VertexId> = self.graph.vertices_with_formulas().collect();
14869        vertices.sort_unstable();
14870        let has_dynamic_refs = vertices.iter().copied().any(|v| self.graph.is_dynamic(v));
14871        let schedule = if vertices.is_empty() {
14872            crate::engine::Schedule {
14873                units: Vec::new(),
14874                layers: Vec::new(),
14875                cycles: Vec::new(),
14876            }
14877        } else {
14878            self.create_evaluation_schedule_uncached(&vertices, None)?.0
14879        };
14880        self.validate_recalc_plan_key(&key)?;
14881        Ok(RecalcPlan {
14882            key,
14883            kind: RecalcPlanKind::CompatibilityFull {
14884                schedule,
14885                has_dynamic_refs,
14886            },
14887        })
14888    }
14889
14890    /// Prepare stable typed targets and retain a revision-bound run-local recipe.
14891    pub fn build_recalc_plan_for_targets(
14892        &mut self,
14893        targets: &[crate::engine::EvaluationTarget],
14894    ) -> Result<RecalcPlan, ExcelError> {
14895        self.build_recalc_plan_for_targets_with_options(
14896            targets,
14897            crate::engine::TargetEvalOptions::default(),
14898        )
14899    }
14900
14901    pub fn build_recalc_plan_for_targets_with_options(
14902        &mut self,
14903        targets: &[crate::engine::EvaluationTarget],
14904        options: crate::engine::TargetEvalOptions<'_>,
14905    ) -> Result<RecalcPlan, ExcelError> {
14906        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
14907            engine.observe_function_semantic_epoch()?;
14908            engine.validate_deterministic_mode()?;
14909            let _source_cache = engine.source_cache_session();
14910            let preparation = engine.prepare_graph_for_routed_evaluation(targets, &options)?;
14911            #[cfg(any(test, feature = "legacy_oracle"))]
14912            engine.graph.flush_pending_edge_deltas();
14913            let topology = if matches!(
14914                preparation.widened_scope,
14915                crate::engine::PrepareScope::Workbook
14916            ) {
14917                RecalcTopology::Workbook
14918            } else {
14919                RecalcTopology::RunLocalRecipe
14920            };
14921            Ok(RecalcPlan {
14922                key: engine.recalc_plan_key(),
14923                kind: RecalcPlanKind::Target {
14924                    targets: targets.to_vec(),
14925                    scope: preparation.widened_scope,
14926                    topology,
14927                    dynamic_policy: DynamicPlanPolicy::BoundedTargetReplan,
14928                },
14929            })
14930        })
14931    }
14932
14933    /// Evaluate using a previously constructed compatibility or target plan.
14934    pub fn evaluate_recalc_plan(&mut self, plan: &RecalcPlan) -> Result<EvalResult, ExcelError> {
14935        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
14936            engine.evaluate_recalc_plan_unobserved(plan)
14937        })
14938    }
14939
14940    pub fn evaluate_recalc_plan_with_controls(
14941        &mut self,
14942        plan: &RecalcPlan,
14943        cancel: Option<crate::engine::CancelToken>,
14944        deadline: Option<Instant>,
14945    ) -> Result<EvalResult, ExcelError> {
14946        self.observe_evaluation_resource_request(EvaluationRequestKind::RecalcPlan, |engine| {
14947            engine.active_cancel_flag = cancel.clone();
14948            engine.active_evaluation_deadline = deadline;
14949            let result = engine.evaluate_recalc_plan_unobserved(plan);
14950            engine.active_cancel_flag = None;
14951            engine.active_evaluation_deadline = None;
14952            result
14953        })
14954    }
14955
14956    fn evaluate_recalc_plan_unobserved(
14957        &mut self,
14958        plan: &RecalcPlan,
14959    ) -> Result<EvalResult, ExcelError> {
14960        #[cfg(any(test, feature = "legacy_oracle"))]
14961        self.graph.flush_pending_edge_deltas();
14962        self.validate_recalc_plan_key(&plan.key)?;
14963        self.cancellation_checkpoint("Evaluation cancelled before recalculation plan execution")?;
14964        self.validate_deterministic_mode()?;
14965
14966        match &plan.kind {
14967            RecalcPlanKind::Target {
14968                targets,
14969                scope,
14970                topology,
14971                dynamic_policy,
14972            } => {
14973                debug_assert_eq!(*dynamic_policy, DynamicPlanPolicy::BoundedTargetReplan);
14974                debug_assert_eq!(
14975                    matches!(topology, RecalcTopology::Workbook),
14976                    matches!(scope, crate::engine::PrepareScope::Workbook)
14977                );
14978                let _source_cache = self.source_cache_session();
14979                self.execute_prepared_target_recipe(targets, scope, None)
14980            }
14981            RecalcPlanKind::CompatibilityFull {
14982                schedule,
14983                has_dynamic_refs,
14984            } => {
14985                let _source_cache = self.source_cache_session();
14986                self.begin_evaluation_request();
14987                if *has_dynamic_refs {
14988                    self.virtual_dep_fallback_activations =
14989                        self.virtual_dep_fallback_activations.saturating_add(1);
14990                    return self.evaluate_all_coordinator();
14991                }
14992
14993                let start = crate::instant::FzInstant::now();
14994                let dirty_vertices = self.graph.get_evaluation_vertices();
14995                if dirty_vertices.is_empty() {
14996                    return Ok(EvalResult {
14997                        computed_vertices: 0,
14998                        cycle_errors: 0,
14999                        elapsed: start.elapsed(),
15000                    });
15001                }
15002
15003                let dirty_set: FxHashSet<VertexId> = dirty_vertices.iter().copied().collect();
15004                let mut computed_vertices = 0;
15005                let mut cycle_errors = 0;
15006                for &unit in &schedule.units {
15007                    self.cancellation_checkpoint(
15008                        "Evaluation cancelled before recalculation plan schedule unit",
15009                    )?;
15010                    match unit {
15011                        ScheduleUnit::Cycle(i) => {
15012                            let stamped = self.handle_cycle_unit(
15013                                schedule.unit_cycle(i),
15014                                None,
15015                                Some(&dirty_set),
15016                                None,
15017                            )?;
15018                            if stamped > 0 {
15019                                cycle_errors += 1;
15020                            }
15021                        }
15022                        ScheduleUnit::Layer(i) => {
15023                            let work: Vec<VertexId> = schedule
15024                                .unit_layer(i)
15025                                .vertices
15026                                .iter()
15027                                .copied()
15028                                .filter(|v| dirty_set.contains(v))
15029                                .collect();
15030                            if work.is_empty() {
15031                                continue;
15032                            }
15033                            let temp_layer = crate::engine::scheduler::Layer::new(work);
15034                            if self.thread_pool.is_some() && temp_layer.vertices.len() > 1 {
15035                                computed_vertices += self.evaluate_layer_parallel(&temp_layer)?;
15036                            } else {
15037                                computed_vertices += self.evaluate_layer_sequential(&temp_layer)?;
15038                            }
15039                        }
15040                    }
15041                }
15042
15043                self.resource_checkpoint(0)?;
15044                self.graph.clear_dirty_flags(&dirty_vertices);
15045                self.redirty_for_next_recalc();
15046                Ok(EvalResult {
15047                    computed_vertices,
15048                    cycle_errors,
15049                    elapsed: start.elapsed(),
15050                })
15051            }
15052        }
15053    }
15054}
15055
15056impl<R> Engine<R>
15057where
15058    R: EvaluationContext,
15059{
15060    /// Refuse out-of-scope authority states before evaluation can demote spans
15061    /// or execute a legacy schedule. The public error type is unchanged; NImpl
15062    /// carries the exact internal Unsupported operation for the deferred-scope
15063    /// gate. Admission and allocation failures are not scope exceptions.
15064    fn require_unified_authority(&mut self) -> Result<(), ExcelError> {
15065        self.graph
15066            .authority()
15067            .map(|_| ())
15068            .map_err(Self::authority_excel_error)
15069    }
15070
15071    fn authority_excel_error(error: crate::engine::authority::store::AuthorityError) -> ExcelError {
15072        use crate::engine::authority::store::AuthorityError;
15073        // Some unchanged behavioral tests assert only `error.kind`, hiding
15074        // the operation in their panic. The opt-in gate trace proves which
15075        // typed error was actually returned; it never changes that error.
15076        #[cfg(test)]
15077        if std::env::var_os("FZ_AUTHORITY_DEFERRED_TRACE").is_some() {
15078            eprintln!("M1B_AUTHORITY_ERROR {error:?}");
15079        }
15080        let kind = match error {
15081            AuthorityError::Unsupported { .. } => ExcelErrorKind::NImpl,
15082            _ => ExcelErrorKind::Error,
15083        };
15084        ExcelError::new(kind).with_message(format!("unified_authority: {error:?}"))
15085    }
15086
15087    /// Evaluate all dirty/volatile vertices
15088    /// Evaluate a current result snapshot, optionally with cancellation.
15089    ///
15090    /// This evaluates exactly like [`Self::evaluate_all`] (or its cancellable
15091    /// counterpart), but leaves volatile vertices and their dependents Current
15092    /// at return rather than marking them dirty for the next cycle. It does
15093    /// not suppress iterative-SCC redirty or any other freshness check.
15094    ///
15095    /// Callers retaining the engine must start another evaluation request
15096    /// (`evaluate_all`, `evaluate_until`, `evaluate_cell`, or equivalent) to
15097    /// refresh volatile values. That request restores the deferred volatile
15098    /// redirty before selecting its work. Errors and cancellation never leave
15099    /// the snapshot mode active.
15100    pub fn evaluate_all_for_snapshot(
15101        &mut self,
15102        cancel: Option<crate::engine::CancelToken>,
15103    ) -> Result<EvalResult, ExcelError> {
15104        /// Leaves snapshot mode even when evaluation unwinds. A panic
15105        /// keeps the deferred volatile redirty pending, so the next request
15106        /// restores volatile work without touching the graph mid-unwind.
15107        struct SnapshotScope<'e, R: EvaluationContext>(&'e mut Engine<R>);
15108        impl<R: EvaluationContext> Drop for SnapshotScope<'_, R> {
15109            fn drop(&mut self) {
15110                self.0.snapshot_evaluation_active = false;
15111                if std::thread::panicking() {
15112                    self.0.snapshot_volatile_redirty_pending = true;
15113                }
15114            }
15115        }
15116        self.snapshot_evaluation_active = true;
15117        let result = {
15118            let scope = SnapshotScope(self);
15119            match cancel {
15120                Some(cancel) => scope.0.evaluate_all_cancellable(cancel),
15121                None => scope.0.evaluate_all(),
15122            }
15123        };
15124        if result.is_err() {
15125            self.snapshot_volatile_redirty_pending = false;
15126            self.graph.redirty_volatiles();
15127        }
15128        result
15129    }
15130
15131    /// Evaluate all dirty/volatile vertices.
15132    pub fn evaluate_all(&mut self) -> Result<EvalResult, ExcelError> {
15133        // `evaluate_all_unobserved` owns the `observe_function_semantic_epoch` guard.
15134        self.observe_evaluation_resource_request(EvaluationRequestKind::Full, |engine| {
15135            engine.evaluate_all_unobserved()
15136        })
15137    }
15138
15139    fn evaluate_all_unobserved(&mut self) -> Result<EvalResult, ExcelError> {
15140        debug_assert!(
15141            !self.graph.deferred_dirty_active(),
15142            "deferred-dirty scope leaked into evaluate_all: a begin_deferred_dirty \
15143             was not balanced by end_deferred_dirty"
15144        );
15145        self.observe_function_semantic_epoch()?;
15146        self.lookup_index_cache.reset_counters();
15147        let _source_cache = self.source_cache_session();
15148        self.validate_deterministic_mode()?;
15149        if self.config.defer_graph_building {
15150            // Build graph for all staged formulas before evaluating
15151            self.build_graph_all()?;
15152        }
15153        self.evaluate_all_coordinator()
15154    }
15155
15156    /// Coordinator for `evaluate_all`: starts the evaluation request and runs
15157    /// the per-cell pass.
15158    fn evaluate_all_coordinator(&mut self) -> Result<EvalResult, ExcelError> {
15159        self.require_unified_authority()?;
15160        self.begin_evaluation_request();
15161        self.evaluate_all_legacy_impl()
15162    }
15163
15164    /// Walk a schedule's units in condensation order: stamp each cyclic SCC
15165    /// at its position and evaluate each layer (parallel when enabled).
15166    ///
15167    /// Returns `(computed_vertices, cycle_count)` where `cycle_count` is the
15168    /// number of Cycle units walked (the former `schedule.cycles.len()`).
15169    fn legacy_pass_run_units(
15170        &mut self,
15171        schedule: &crate::engine::scheduler::Schedule,
15172    ) -> Result<(usize, usize), ExcelError> {
15173        let mut computed_vertices = 0;
15174        let mut cycle_count = 0;
15175        self.begin_pass(schedule);
15176        for (unit_index, &unit) in schedule.units.iter().enumerate() {
15177            match unit {
15178                ScheduleUnit::Cycle(i) => {
15179                    if self.handle_cycle_unit(schedule.unit_cycle(i), None, None, None)? > 0 {
15180                        cycle_count += 1;
15181                    }
15182                }
15183                ScheduleUnit::Layer(i) => {
15184                    let layer = schedule.unit_layer(i);
15185                    if self.thread_pool.is_some() && layer.vertices.len() > 1 {
15186                        computed_vertices += self.evaluate_layer_parallel(layer)?;
15187                    } else {
15188                        computed_vertices += self.evaluate_layer_sequential(layer)?;
15189                    }
15190                }
15191            }
15192            if self.stop_after_unit(schedule, unit_index) {
15193                break;
15194            }
15195        }
15196        Ok((computed_vertices, cycle_count))
15197    }
15198
15199    /// Per-cell `evaluate_all` body, reached through the coordinator. This is
15200    /// an internal primitive; it must not be invoked directly from public APIs.
15201    ///
15202    /// Does NOT call `begin_evaluation_request` (cycle-telemetry reset +
15203    /// per-recalc clock sample): request begin happens at the public entry
15204    /// points / coordinators, so one request keeps one clock sample.
15205    fn evaluate_all_legacy_impl(&mut self) -> Result<EvalResult, ExcelError> {
15206        self.reset_virtual_dep_telemetry_if_disabled();
15207        let _span_eval =
15208            crate::engine::trace::fz_span!(tracing::Level::INFO, "evaluate", "evaluate.legacy");
15209        let start = crate::instant::FzInstant::now();
15210        let mut computed_vertices = 0;
15211        let mut cycle_errors = 0;
15212        let mut replan_iterations = 0;
15213        const MAX_REPLAN: usize = 5;
15214        let mut telemetry = self
15215            .config
15216            .enable_virtual_dep_telemetry
15217            .then(|| self.start_virtual_dep_telemetry());
15218
15219        loop {
15220            let to_evaluate = self.graph.get_evaluation_vertices();
15221            if to_evaluate.is_empty() {
15222                if let Some(t) = telemetry.as_mut()
15223                    && t.bailout_reason.is_none()
15224                {
15225                    t.bailout_reason = Some("no_work");
15226                }
15227                break;
15228            }
15229
15230            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
15231            if let Some(t) = telemetry.as_mut() {
15232                Self::accumulate_schedule_meta(t, &meta);
15233            }
15234
15235            let (pass_computed, pass_cycles) = self.legacy_pass_run_units(&schedule)?;
15236            computed_vertices += pass_computed;
15237            cycle_errors += pass_cycles;
15238
15239            // Check if dynamic dependencies changed
15240            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
15241            if let Some(t) = telemetry.as_mut() {
15242                t.changed_vdeps_total += changed_vertices.len();
15243            }
15244
15245            self.resource_checkpoint(0)?;
15246            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
15247                if let Some(t) = telemetry.as_mut() {
15248                    t.bailout_reason = Some("converged");
15249                }
15250                break;
15251            }
15252            if replan_iterations >= MAX_REPLAN {
15253                if let Some(mut t) = telemetry.take() {
15254                    t.bailout_reason = Some("max_replan");
15255                    t.replan_iterations = replan_iterations;
15256                    self.last_virtual_dep_telemetry = t;
15257                }
15258                return Err(
15259                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
15260                );
15261            }
15262
15263            replan_iterations += 1;
15264        }
15265
15266        if let Some(mut t) = telemetry {
15267            t.replan_iterations = replan_iterations;
15268            self.last_virtual_dep_telemetry = t;
15269        }
15270
15271        // Re-dirty volatile vertices for the next evaluation cycle
15272        self.redirty_for_next_recalc();
15273
15274        // Advance recalc epoch after a full evaluation pass finishes
15275        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
15276
15277        Ok(EvalResult {
15278            computed_vertices,
15279            cycle_errors,
15280            elapsed: start.elapsed(),
15281        })
15282    }
15283
15284    pub fn evaluate_all_with_target_delta(
15285        &mut self,
15286    ) -> Result<(EvalResult, crate::engine::TargetEvalDelta), ExcelError> {
15287        self.observe_evaluation_resource_request(EvaluationRequestKind::FullWithDelta, |engine| {
15288            engine.observe_function_semantic_epoch()?;
15289            let mut collector = DeltaCollector::new(DeltaMode::Cells);
15290            let result = engine.evaluate_all_with_delta_collector(&mut collector)?;
15291            Ok((result, collector.finish_target()))
15292        })
15293    }
15294
15295    pub fn evaluate_all_with_delta(&mut self) -> Result<(EvalResult, EvalDelta), ExcelError> {
15296        self.evaluate_all_with_delta_policy(EvalDeltaCompatibilityPolicy::Unlimited)
15297    }
15298
15299    pub fn evaluate_all_with_delta_policy(
15300        &mut self,
15301        policy: EvalDeltaCompatibilityPolicy,
15302    ) -> Result<(EvalResult, EvalDelta), ExcelError> {
15303        self.observe_evaluation_resource_request(EvaluationRequestKind::FullWithDelta, |engine| {
15304            engine.observe_function_semantic_epoch()?;
15305            let mut collector = DeltaCollector::new(DeltaMode::Cells);
15306            let result = engine.evaluate_all_with_delta_collector(&mut collector)?;
15307            Ok((result, collector.finish_with_policy(policy)?))
15308        })
15309    }
15310
15311    fn evaluate_all_with_delta_collector(
15312        &mut self,
15313        delta: &mut DeltaCollector,
15314    ) -> Result<EvalResult, ExcelError> {
15315        let _source_cache = self.source_cache_session();
15316        if self.config.defer_graph_building {
15317            self.build_graph_all()?;
15318        }
15319        self.require_unified_authority()?;
15320        self.begin_evaluation_request();
15321        self.reset_virtual_dep_telemetry_if_disabled();
15322        let _span_eval = crate::engine::trace::fz_span!(
15323            tracing::Level::INFO,
15324            "evaluate",
15325            "evaluate.legacy_delta"
15326        );
15327        let start = crate::instant::FzInstant::now();
15328        let mut computed_vertices = 0;
15329        let mut cycle_errors = 0;
15330
15331        let mut replan_iterations = 0;
15332        const MAX_REPLAN: usize = 5;
15333        let mut telemetry = self
15334            .config
15335            .enable_virtual_dep_telemetry
15336            .then(|| self.start_virtual_dep_telemetry());
15337
15338        loop {
15339            let to_evaluate = self.graph.get_evaluation_vertices();
15340            if to_evaluate.is_empty() {
15341                if let Some(t) = telemetry.as_mut()
15342                    && t.bailout_reason.is_none()
15343                {
15344                    t.bailout_reason = Some("no_work");
15345                }
15346                break;
15347            }
15348
15349            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
15350            if let Some(t) = telemetry.as_mut() {
15351                Self::accumulate_schedule_meta(t, &meta);
15352            }
15353
15354            self.begin_pass(&schedule);
15355            for (unit_index, &unit) in schedule.units.iter().enumerate() {
15356                match unit {
15357                    ScheduleUnit::Cycle(i) => {
15358                        if self.handle_cycle_unit(
15359                            schedule.unit_cycle(i),
15360                            Some(delta),
15361                            None,
15362                            None,
15363                        )? > 0
15364                        {
15365                            cycle_errors += 1;
15366                        }
15367                    }
15368                    ScheduleUnit::Layer(i) => {
15369                        let layer = schedule.unit_layer(i);
15370                        if self.thread_pool.is_some() && layer.vertices.len() > 1 {
15371                            computed_vertices +=
15372                                self.evaluate_layer_parallel_with_delta(layer, delta)?;
15373                        } else {
15374                            computed_vertices +=
15375                                self.evaluate_layer_sequential_with_delta(layer, delta)?;
15376                        }
15377                    }
15378                }
15379                if self.stop_after_unit(&schedule, unit_index) {
15380                    break;
15381                }
15382            }
15383
15384            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
15385            if let Some(t) = telemetry.as_mut() {
15386                t.changed_vdeps_total += changed_vertices.len();
15387            }
15388            self.resource_checkpoint(0)?;
15389            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
15390                if let Some(t) = telemetry.as_mut() {
15391                    t.bailout_reason = Some("converged");
15392                }
15393                break;
15394            }
15395            if replan_iterations >= MAX_REPLAN {
15396                if let Some(mut t) = telemetry.take() {
15397                    t.bailout_reason = Some("max_replan");
15398                    t.replan_iterations = replan_iterations;
15399                    self.last_virtual_dep_telemetry = t;
15400                }
15401                return Err(
15402                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
15403                );
15404            }
15405            replan_iterations += 1;
15406        }
15407
15408        if let Some(mut t) = telemetry {
15409            t.replan_iterations = replan_iterations;
15410            self.last_virtual_dep_telemetry = t;
15411        }
15412
15413        self.redirty_for_next_recalc();
15414        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
15415
15416        Ok(EvalResult {
15417            computed_vertices,
15418            cycle_errors,
15419            elapsed: start.elapsed(),
15420        })
15421    }
15422
15423    /// Convenience: demand-driven evaluation of a single cell by sheet name and row/col.
15424    ///
15425    /// This will evaluate only the minimal set of dirty / volatile precedents required
15426    /// to bring the target cell up-to-date (as if a user asked for that single value),
15427    /// rather than scheduling a full workbook recalc. If the cell is already clean and
15428    /// non-volatile, no vertices will be recomputed.
15429    ///
15430    /// Returns the (possibly newly computed) value stored for the cell afterwards.
15431    /// Empty cells return None. Errors are surfaced via the Result type.
15432    pub fn evaluate_cell(
15433        &mut self,
15434        sheet: &str,
15435        row: u32,
15436        col: u32,
15437    ) -> Result<Option<LiteralValue>, ExcelError> {
15438        self.observe_evaluation_resource_request(EvaluationRequestKind::Cell, |engine| {
15439            engine.evaluate_cell_unobserved(sheet, row, col)
15440        })
15441    }
15442
15443    fn evaluate_cell_unobserved(
15444        &mut self,
15445        sheet: &str,
15446        row: u32,
15447        col: u32,
15448    ) -> Result<Option<LiteralValue>, ExcelError> {
15449        if row == 0 || col == 0 {
15450            return Err(ExcelError::new(ExcelErrorKind::Ref)
15451                .with_message("Row and column must be >= 1".to_string()));
15452        }
15453
15454        let result = self.evaluate_cells(&[(sheet, row, col)])?;
15455
15456        match result.len() {
15457            0 => Ok(None),
15458            1 => {
15459                let v = result.into_iter().next().unwrap();
15460                Ok(v)
15461            }
15462            _ => unreachable!("evaluate_cells returned unexpected length"),
15463        }
15464    }
15465
15466    /// Convenience: demand-driven evaluation of multiple cells; accepts a slice of
15467    /// (sheet, row, col) triples. The union of required dirty / volatile precedents
15468    /// is computed once and evaluated, which is typically faster than calling
15469    /// `evaluate_cell` repeatedly for a related set of targets.
15470    ///
15471    /// Returns the resulting values for each requested target in the same order.
15472    pub fn evaluate_cells(
15473        &mut self,
15474        targets: &[(&str, u32, u32)],
15475    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
15476        self.observe_evaluation_resource_request(EvaluationRequestKind::Cells, |engine| {
15477            engine.evaluate_cells_unobserved(targets)
15478        })
15479    }
15480
15481    fn evaluate_cells_unobserved(
15482        &mut self,
15483        targets: &[(&str, u32, u32)],
15484    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
15485        self.observe_function_semantic_epoch()?;
15486        debug_assert!(
15487            !self.graph.deferred_dirty_active(),
15488            "deferred-dirty scope leaked into evaluate_cells: a begin_deferred_dirty \
15489             was not balanced by end_deferred_dirty"
15490        );
15491        self.validate_deterministic_mode()?;
15492        if targets.is_empty() {
15493            return Ok(Vec::new());
15494        }
15495        let typed_targets = self.legacy_coordinate_targets(targets);
15496        self.evaluate_mixed_targets(&typed_targets, None)?;
15497        Ok(targets
15498            .iter()
15499            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
15500            .collect())
15501    }
15502
15503    pub fn evaluate_cells_cancellable(
15504        &mut self,
15505        targets: &[(&str, u32, u32)],
15506        cancel: crate::engine::CancelToken,
15507    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
15508        self.observe_evaluation_resource_request(
15509            EvaluationRequestKind::CellsCancellable,
15510            |engine| {
15511                engine.observe_function_semantic_epoch()?;
15512                engine.active_cancel_flag = Some(cancel.clone());
15513                let res = engine.evaluate_cells_cancellable_impl(targets, cancel.as_flag());
15514                engine.active_cancel_flag = None;
15515                res
15516            },
15517        )
15518    }
15519
15520    fn evaluate_cells_cancellable_impl(
15521        &mut self,
15522        targets: &[(&str, u32, u32)],
15523        cancel_flag: &AtomicBool,
15524    ) -> Result<Vec<Option<LiteralValue>>, ExcelError> {
15525        self.validate_deterministic_mode()?;
15526        if targets.is_empty() {
15527            return Ok(Vec::new());
15528        }
15529        if cancel_flag.load(Ordering::Relaxed) {
15530            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
15531                .with_message("Evaluation cancelled before target preparation"));
15532        }
15533        let typed_targets = self.legacy_coordinate_targets(targets);
15534        self.evaluate_mixed_targets(&typed_targets, None)?;
15535        Ok(targets
15536            .iter()
15537            .map(|(sheet, row, col)| self.get_cell_value(sheet, *row, *col))
15538            .collect())
15539    }
15540
15541    pub fn evaluate_cells_with_target_delta(
15542        &mut self,
15543        targets: &[(&str, u32, u32)],
15544    ) -> Result<(Vec<Option<LiteralValue>>, crate::engine::TargetEvalDelta), ExcelError> {
15545        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
15546            engine.observe_function_semantic_epoch()?;
15547            engine.validate_deterministic_mode()?;
15548            if targets.is_empty() {
15549                return Ok((Vec::new(), crate::engine::TargetEvalDelta::default()));
15550            }
15551            let mut collector = DeltaCollector::new(DeltaMode::Cells);
15552            engine.evaluate_until_with_delta_collector(targets, &mut collector)?;
15553            let values = targets
15554                .iter()
15555                .map(|(sheet, row, col)| engine.get_cell_value(sheet, *row, *col))
15556                .collect();
15557            Ok((values, collector.finish_target()))
15558        })
15559    }
15560
15561    pub fn evaluate_cells_with_delta(
15562        &mut self,
15563        targets: &[(&str, u32, u32)],
15564    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
15565        self.evaluate_cells_with_delta_policy(targets, EvalDeltaCompatibilityPolicy::Unlimited)
15566    }
15567
15568    pub fn evaluate_cells_with_delta_policy(
15569        &mut self,
15570        targets: &[(&str, u32, u32)],
15571        policy: EvalDeltaCompatibilityPolicy,
15572    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
15573        self.observe_evaluation_resource_request(EvaluationRequestKind::CellsWithDelta, |engine| {
15574            engine.evaluate_cells_with_delta_unobserved(targets, policy)
15575        })
15576    }
15577
15578    fn evaluate_cells_with_delta_unobserved(
15579        &mut self,
15580        targets: &[(&str, u32, u32)],
15581        policy: EvalDeltaCompatibilityPolicy,
15582    ) -> Result<(Vec<Option<LiteralValue>>, EvalDelta), ExcelError> {
15583        self.observe_function_semantic_epoch()?;
15584        self.validate_deterministic_mode()?;
15585        if targets.is_empty() {
15586            return Ok((Vec::new(), EvalDelta::default()));
15587        }
15588        let mut collector = DeltaCollector::new(DeltaMode::Cells);
15589        self.evaluate_until_with_delta_collector(targets, &mut collector)?;
15590        let values = targets
15591            .iter()
15592            .map(|(s, r, c)| self.get_cell_value(s, *r, *c))
15593            .collect();
15594        Ok((values, collector.finish_with_policy(policy)?))
15595    }
15596
15597    /// Get the evaluation plan for target cells without actually evaluating them
15598    pub fn get_eval_plan(&self, targets: &[(&str, u32, u32)]) -> Result<EvalPlan, ExcelError> {
15599        if targets.is_empty() {
15600            return Ok(EvalPlan {
15601                total_vertices_to_evaluate: 0,
15602                layers: Vec::new(),
15603                cycles_detected: 0,
15604                dirty_count: 0,
15605                volatile_count: 0,
15606                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
15607                estimated_parallel_layers: 0,
15608                target_cells: Vec::new(),
15609            });
15610        }
15611        if self.config.defer_graph_building && self.has_staged_formulas() {
15612            return Err(ExcelError::new(ExcelErrorKind::Value).with_message(
15613                "Evaluation plan requested with deferred graph; build first or call evaluate_*",
15614            ));
15615        }
15616
15617        // Convert targets to A1 notation for consistency
15618        let addresses: Vec<String> = targets
15619            .iter()
15620            .map(|(s, r, c)| format!("{}!{}{}", s, Self::col_to_letters(*c), r))
15621            .collect();
15622
15623        // Parse target cell addresses
15624        let mut target_addrs = Vec::new();
15625        for (sheet, row, col) in targets {
15626            if let Some(sheet_id) = self.graph.sheet_id(sheet) {
15627                let coord = Coord::from_excel(*row, *col, true, true);
15628                target_addrs.push(CellRef::new(sheet_id, coord));
15629            }
15630        }
15631
15632        // Find vertex IDs for targets
15633        let mut target_vertex_ids = Vec::new();
15634        for addr in &target_addrs {
15635            if let Some(vertex_id) = self.graph.get_vertex_id_for_address(addr) {
15636                target_vertex_ids.push(vertex_id);
15637            }
15638        }
15639
15640        if target_vertex_ids.is_empty() {
15641            return Ok(EvalPlan {
15642                total_vertices_to_evaluate: 0,
15643                layers: Vec::new(),
15644                cycles_detected: 0,
15645                dirty_count: 0,
15646                volatile_count: 0,
15647                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
15648                estimated_parallel_layers: 0,
15649                target_cells: addresses,
15650            });
15651        }
15652
15653        // Build demand subgraph with virtual edges (same as evaluate_until)
15654        let (precedents_to_eval, vdeps) = self.demand_subgraph(&target_vertex_ids)?;
15655
15656        if precedents_to_eval.is_empty() {
15657            return Ok(EvalPlan {
15658                total_vertices_to_evaluate: 0,
15659                layers: Vec::new(),
15660                cycles_detected: 0,
15661                dirty_count: 0,
15662                volatile_count: 0,
15663                parallel_enabled: self.config.enable_parallel && self.thread_pool.is_some(),
15664                estimated_parallel_layers: 0,
15665                target_cells: addresses,
15666            });
15667        }
15668
15669        // Count dirty and volatile vertices
15670        let mut dirty_count = 0;
15671        let mut volatile_count = 0;
15672        for &vertex_id in &precedents_to_eval {
15673            if self.graph.is_dirty(vertex_id) {
15674                dirty_count += 1;
15675            }
15676            if self.graph.is_volatile(vertex_id) {
15677                volatile_count += 1;
15678            }
15679        }
15680
15681        // Create schedule for the minimal subgraph honoring virtual edges
15682        let schedule = self.create_authority_schedule(&precedents_to_eval, &vdeps, None)?;
15683
15684        // Build layer information
15685        let mut layers = Vec::new();
15686        let mut estimated_parallel_layers = 0;
15687        let parallel_enabled = self.config.enable_parallel && self.thread_pool.is_some();
15688
15689        for layer in &schedule.layers {
15690            let parallel_eligible = parallel_enabled && layer.vertices.len() > 1;
15691            if parallel_eligible {
15692                estimated_parallel_layers += 1;
15693            }
15694
15695            // Get sample cell addresses (up to 5)
15696            let sample_cells: Vec<String> = layer
15697                .vertices
15698                .iter()
15699                .take(5)
15700                .filter_map(|&vertex_id| {
15701                    self.graph
15702                        .get_cell_ref_for_vertex(vertex_id)
15703                        .map(|cell_ref| {
15704                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
15705                            format!(
15706                                "{}!{}{}",
15707                                sheet_name,
15708                                Self::col_to_letters(cell_ref.coord.col().saturating_add(1)),
15709                                cell_ref.coord.row() + 1
15710                            )
15711                        })
15712                })
15713                .collect();
15714
15715            layers.push(LayerInfo {
15716                vertex_count: layer.vertices.len(),
15717                parallel_eligible,
15718                sample_cells,
15719            });
15720        }
15721
15722        Ok(EvalPlan {
15723            total_vertices_to_evaluate: precedents_to_eval.len(),
15724            layers,
15725            cycles_detected: schedule.cycles.len(),
15726            dirty_count,
15727            volatile_count,
15728            parallel_enabled,
15729            estimated_parallel_layers,
15730            target_cells: addresses,
15731        })
15732    }
15733    /// Helper to create a schedule, integrating virtual dependencies automatically.
15734    fn create_evaluation_schedule(
15735        &mut self,
15736        to_evaluate: &[VertexId],
15737    ) -> Result<EvaluationScheduleBuildOutput, ExcelError> {
15738        #[cfg(any(test, feature = "benchmark_internal"))]
15739        {
15740            self.recalc_reuse_probe.get_mut().unwrap().schedule_requests += 1;
15741        }
15742        // Fold pending edge deltas once per schedule build so traversal uses
15743        // the zero-allocation CSR slices (#125).
15744        #[cfg(any(test, feature = "legacy_oracle"))]
15745        self.graph.flush_pending_edge_deltas();
15746        // The cache key includes the authority revision: sync first.
15747        self.graph.authority_sync();
15748        if self.can_use_static_schedule_cache(to_evaluate) {
15749            // A recent schedule for the same request becomes the current one.
15750            let revision = self.schedule_cache_authority_revision();
15751            let current = |e: &CachedScheduleEntry| {
15752                e.topology_epoch == self.topology_epoch && e.authority_revision == revision
15753            };
15754            if !self
15755                .cached_static_schedule
15756                .as_ref()
15757                .is_some_and(|c| current(c) && c.candidate_vertices.equals(to_evaluate))
15758                && let Some(i) = self.recent_schedules.iter().position(|e| {
15759                    current(e)
15760                        && e.candidate_vertices.len() == to_evaluate.len()
15761                        && e.candidate_vertices.equals(to_evaluate)
15762                })
15763            {
15764                let hit = self.recent_schedules.remove(i);
15765                if let Some(previous) = self.cached_static_schedule.replace(hit) {
15766                    self.retain_recent_schedule(previous);
15767                }
15768            }
15769            if let Some(cached) = self.cached_static_schedule.as_ref()
15770                && cached.topology_epoch == self.topology_epoch
15771                && cached.authority_revision == self.schedule_cache_authority_revision()
15772                && cached.candidate_vertices.equals(to_evaluate)
15773            {
15774                let meta = ScheduleBuildMeta {
15775                    candidate_vertices: to_evaluate.len(),
15776                    vdeps_vertices: 0,
15777                    vdeps_edges: 0,
15778                    builder_elapsed_ms: 0,
15779                    used_virtual_schedule: false,
15780                    schedule_cache_hit: true,
15781                    schedule_cache_eligible: true,
15782                };
15783                #[cfg(any(test, feature = "benchmark_internal"))]
15784                {
15785                    let mut probe = self.recalc_reuse_probe.lock().unwrap();
15786                    probe.schedule_cache_hits += 1;
15787                    probe.schedule_shared_handles += 1;
15788                }
15789                return Ok((
15790                    EvaluationSchedule::Shared(Arc::clone(&cached.schedule)),
15791                    FxHashMap::default(),
15792                    meta,
15793                ));
15794            }
15795
15796            let (schedule, vdeps, mut meta) = match self.schedule_from_base(to_evaluate)? {
15797                Some(schedule) => (
15798                    schedule,
15799                    FxHashMap::default(),
15800                    ScheduleBuildMeta {
15801                        candidate_vertices: to_evaluate.len(),
15802                        vdeps_vertices: 0,
15803                        vdeps_edges: 0,
15804                        builder_elapsed_ms: 0,
15805                        used_virtual_schedule: false,
15806                        schedule_cache_hit: false,
15807                        schedule_cache_eligible: true,
15808                    },
15809                ),
15810                None => self.create_evaluation_schedule_active(to_evaluate)?,
15811            };
15812            meta.schedule_cache_hit = false;
15813            meta.schedule_cache_eligible = true;
15814            #[cfg(any(test, feature = "benchmark_internal"))]
15815            {
15816                self.recalc_reuse_probe
15817                    .get_mut()
15818                    .unwrap()
15819                    .schedule_cache_misses += 1;
15820            }
15821            let schedule = if vdeps.is_empty() {
15822                // Clone previously discarded builder spare capacity. Keep that compact
15823                // retained payload while sharing it with the current request.
15824                let mut schedule = schedule;
15825                schedule.units.shrink_to_fit();
15826                for layer in &mut schedule.layers {
15827                    layer.vertices.shrink_to_fit();
15828                }
15829                schedule.layers.shrink_to_fit();
15830                for cycle in &mut schedule.cycles {
15831                    cycle.shrink_to_fit();
15832                }
15833                schedule.cycles.shrink_to_fit();
15834                let schedule = Arc::new(schedule);
15835                #[cfg(any(test, feature = "benchmark_internal"))]
15836                {
15837                    self.recalc_reuse_probe
15838                        .get_mut()
15839                        .unwrap()
15840                        .schedule_shared_handles += 1;
15841                }
15842                let entry = CachedScheduleEntry {
15843                    topology_epoch: self.topology_epoch,
15844                    authority_revision: self.schedule_cache_authority_revision(),
15845                    candidate_vertices: VertexIdRuns::from_slice(to_evaluate),
15846                    schedule: Arc::clone(&schedule),
15847                };
15848                if let Some(previous) = self.cached_static_schedule.replace(entry) {
15849                    self.retain_recent_schedule(previous);
15850                }
15851                EvaluationSchedule::Shared(schedule)
15852            } else {
15853                EvaluationSchedule::Owned(schedule)
15854            };
15855            return Ok((schedule, vdeps, meta));
15856        }
15857
15858        let (schedule, vdeps, mut meta) = self.create_evaluation_schedule_active(to_evaluate)?;
15859        meta.schedule_cache_hit = false;
15860        meta.schedule_cache_eligible = false;
15861        #[cfg(any(test, feature = "benchmark_internal"))]
15862        {
15863            self.recalc_reuse_probe
15864                .get_mut()
15865                .unwrap()
15866                .schedule_cache_ineligible += 1;
15867        }
15868        Ok((EvaluationSchedule::Owned(schedule), vdeps, meta))
15869    }
15870
15871    /// Plan reuse: the base schedule restricted to `to_evaluate` when it
15872    /// is current, covers the request, has at least
15873    /// `BASE_SCHEDULE_MIN_REQUEST` candidates, and is at most
15874    /// `BASE_SCHEDULE_RATIO` times its size (restricting walks the whole
15875    /// base; planning costs far more per candidate).
15876    fn schedule_from_base(
15877        &mut self,
15878        to_evaluate: &[VertexId],
15879    ) -> Result<Option<crate::engine::scheduler::Schedule>, ExcelError> {
15880        let revision = self.schedule_cache_authority_revision();
15881        let current = |e: &&CachedScheduleEntry| {
15882            e.topology_epoch == self.topology_epoch && e.authority_revision == revision
15883        };
15884        // The base, or the current schedule when larger (the first
15885        // evaluation's, before a later request replaces it).
15886        let Some(base) = [
15887            self.base_schedule.as_ref(),
15888            self.cached_static_schedule.as_ref(),
15889        ]
15890        .into_iter()
15891        .flatten()
15892        .filter(current)
15893        .max_by_key(|e| e.candidate_vertices.len()) else {
15894            return Ok(None);
15895        };
15896        let base_len = base.candidate_vertices.len();
15897        if base_len <= BASE_SCHEDULE_MIN_VERTICES
15898            || to_evaluate.len() < BASE_SCHEDULE_MIN_REQUEST
15899            || to_evaluate.len() > base_len
15900            || base_len > to_evaluate.len().saturating_mul(BASE_SCHEDULE_RATIO)
15901        {
15902            return Ok(None);
15903        }
15904        let mut keep = crate::engine::idset::DenseIdSet::default();
15905        keep.extend(to_evaluate.iter().copied());
15906        let Some((schedule, kept)) = base.schedule.restrict(&keep) else {
15907            return Ok(None);
15908        };
15909        // Every requested vertex must be in the base.
15910        if kept != keep.len() {
15911            return Ok(None);
15912        }
15913        if let Some(ledger) = self.active_resource_ledger.as_mut() {
15914            let layers: usize = schedule
15915                .layers
15916                .iter()
15917                .map(|l| {
15918                    l.vertices.capacity() * std::mem::size_of::<VertexId>()
15919                        + l.runs.capacity()
15920                            * std::mem::size_of::<crate::engine::scheduler::LayerRun>()
15921                })
15922                .sum();
15923            let bytes = (keep.heap_bytes()
15924                + layers
15925                + schedule.layers.capacity()
15926                    * std::mem::size_of::<crate::engine::scheduler::Layer>()
15927                + schedule.units.capacity()
15928                    * std::mem::size_of::<crate::engine::scheduler::ScheduleUnit>()
15929                + schedule
15930                    .cycles
15931                    .iter()
15932                    .map(|c| c.capacity() * 4)
15933                    .sum::<usize>()) as u64;
15934            ledger
15935                .reserve_schedule_discovery(bytes)
15936                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15937            ledger
15938                .release_scratch(bytes)
15939                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
15940        }
15941        #[cfg(debug_assertions)]
15942        self.debug_check_restricted_schedule(to_evaluate, &schedule);
15943        #[cfg(any(test, feature = "benchmark_internal"))]
15944        {
15945            self.recalc_reuse_probe
15946                .get_mut()
15947                .unwrap()
15948                .schedule_base_restrictions += 1;
15949        }
15950        Ok(Some(schedule))
15951    }
15952
15953    /// Debug builds: a restricted schedule holds each requested vertex
15954    /// once and orders the request like a freshly planned schedule (every
15955    /// dependency among the request in an earlier unit).
15956    #[cfg(debug_assertions)]
15957    fn debug_check_restricted_schedule(
15958        &self,
15959        to_evaluate: &[VertexId],
15960        schedule: &crate::engine::scheduler::Schedule,
15961    ) {
15962        let mut position: FxHashMap<VertexId, usize> = FxHashMap::default();
15963        for (u, unit) in schedule.units.iter().enumerate() {
15964            let vertices: &[VertexId] = match *unit {
15965                crate::engine::scheduler::ScheduleUnit::Layer(i) => {
15966                    &schedule.unit_layer(i).vertices
15967                }
15968                crate::engine::scheduler::ScheduleUnit::Cycle(i) => schedule.unit_cycle(i),
15969            };
15970            for &v in vertices {
15971                assert!(
15972                    position.insert(v, u).is_none(),
15973                    "vertex twice in a restricted schedule"
15974                );
15975            }
15976        }
15977        assert_eq!(position.len(), to_evaluate.len());
15978        if to_evaluate.len() > 512 {
15979            return;
15980        }
15981        // Every arc among the request (the store's edge images) goes to an
15982        // earlier unit, or within one sequential layer (a chain) or cycle.
15983        let Ok(store) = self.graph.authority_plan_store() else {
15984            return;
15985        };
15986        let cells: Vec<(VertexId, (u16, u32, u32))> = to_evaluate
15987            .iter()
15988            .filter_map(|&v| self.graph.authority_cell_of_vertex(v).map(|c| (v, c)))
15989            .collect();
15990        for &(v, (sheet, row, col)) in &cells {
15991            let Some(owner) = store.owner_at((sheet, row, col)) else {
15992                continue;
15993            };
15994            let Ok(refined) = store.refine_owner_column(owner, col, row, row, None) else {
15995                continue;
15996            };
15997            for piece in &refined.pieces {
15998                for edge in &refined.edges[piece.edge_start..piece.edge_end] {
15999                    let Some(image) = edge.proj.forward(&piece.domain) else {
16000                        continue;
16001                    };
16002                    for &(d, (s2, r2, c2)) in &cells {
16003                        if d == v
16004                            || edge.proj.sheet != s2
16005                            || !(image.r0 <= r2
16006                                && r2 <= image.r1
16007                                && image.c0 <= c2
16008                                && c2 <= image.c1)
16009                        {
16010                            continue;
16011                        }
16012                        let (pd, pv) = (position[&d], position[&v]);
16013                        let same_ok = pd == pv
16014                            && match schedule.units[pv] {
16015                                crate::engine::scheduler::ScheduleUnit::Layer(i) => {
16016                                    schedule.unit_layer(i).sequential
16017                                }
16018                                crate::engine::scheduler::ScheduleUnit::Cycle(_) => true,
16019                            };
16020                        assert!(
16021                            pd < pv || same_ok,
16022                            "restricted schedule orders {v:?} before its precedent {d:?}"
16023                        );
16024                    }
16025                }
16026            }
16027        }
16028    }
16029
16030    /// Compress family formulas once per authority build (see
16031    /// `EvalConfig::formula_compression`), when no staged or deferred
16032    /// formula package can hold arena ids.
16033    fn maybe_compress_formulas(&mut self) {
16034        if !self.config.formula_compression {
16035            return;
16036        }
16037        let builds = self.graph.authority_host().builds;
16038        if self.compressed_at_build == Some(builds) {
16039            return;
16040        }
16041        self.compressed_at_build = Some(builds);
16042        if self.has_staged_formulas() {
16043            // Staged packages hold arena ids: no compaction. Members that
16044            // are already compressed can still leave the per-cell maps.
16045            self.graph.virtualize_family_members();
16046            return;
16047        }
16048        let pool = self.thread_pool.clone();
16049        let (_, garbage) = self.graph.compress_family_formulas(pool.as_deref());
16050        self.graph.virtualize_family_members();
16051        // Freeing the dropped members' reference texts (one allocation each)
16052        // is most of compaction; with a pool it happens off the critical
16053        // path.
16054        match pool {
16055            Some(pool) if garbage.len() >= 1024 => pool.spawn(move || drop(garbage)),
16056            _ => drop(garbage),
16057        }
16058    }
16059
16060    fn create_evaluation_schedule_active(
16061        &mut self,
16062        to_evaluate: &[VertexId],
16063    ) -> Result<ScheduleBuildOutput, ExcelError> {
16064        self.graph.authority_sync();
16065        self.maybe_compress_formulas();
16066        let mut ledger = self.active_resource_ledger.take();
16067        let result = self.create_evaluation_schedule_uncached(to_evaluate, ledger.as_mut());
16068        self.active_resource_ledger = ledger;
16069        result
16070    }
16071
16072    fn create_evaluation_schedule_uncached(
16073        &self,
16074        to_evaluate: &[VertexId],
16075        #[allow(unused_variables)] ledger: Option<&mut ResourceLedger>,
16076    ) -> Result<ScheduleBuildOutput, ExcelError> {
16077        #[cfg(any(test, feature = "benchmark_internal"))]
16078        {
16079            self.recalc_reuse_probe.lock().unwrap().schedule_builds += 1;
16080        }
16081        let builder = VirtualDepBuilder::new(self);
16082        #[allow(unused_mut)]
16083        let (mut vdeps, augmented, builder_elapsed_ms, vdeps_edges) =
16084            if self.config.enable_virtual_dep_telemetry {
16085                let build_started = crate::instant::FzInstant::now();
16086                let (vdeps, augmented) = builder.build(to_evaluate);
16087                let builder_elapsed_ms = build_started.elapsed().as_millis();
16088                let vdeps_edges = vdeps.values().map(|deps| deps.len()).sum::<usize>();
16089                (vdeps, augmented, builder_elapsed_ms, vdeps_edges)
16090            } else {
16091                let (vdeps, augmented) = builder.build(to_evaluate);
16092                (vdeps, augmented, 0, 0)
16093            };
16094
16095        // Replan hints from stale dynamic reads earlier in this request.
16096        {
16097            self.freshness_merge_hints(to_evaluate, &mut vdeps);
16098            self.freshness_extent_hints(to_evaluate, &mut vdeps);
16099        }
16100        let mut final_evaluate = to_evaluate.to_vec();
16101        if !augmented.is_empty() {
16102            final_evaluate.extend(augmented);
16103            final_evaluate.sort_unstable();
16104            final_evaluate.dedup();
16105        }
16106
16107        let use_virtual = !vdeps.is_empty();
16108
16109        let schedule = self.create_authority_schedule(&final_evaluate, &vdeps, ledger)?;
16110
16111        let meta = ScheduleBuildMeta {
16112            candidate_vertices: to_evaluate.len(),
16113            vdeps_vertices: vdeps.len(),
16114            vdeps_edges,
16115            builder_elapsed_ms,
16116            used_virtual_schedule: use_virtual,
16117            schedule_cache_hit: false,
16118            schedule_cache_eligible: false,
16119        };
16120
16121        Ok((schedule, vdeps, meta))
16122    }
16123
16124    fn create_authority_schedule(
16125        &self,
16126        candidates: &[VertexId],
16127        vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
16128        mut ledger: Option<&mut ResourceLedger>,
16129    ) -> Result<crate::engine::scheduler::Schedule, ExcelError> {
16130        use crate::engine::authority::{
16131            geom::{Cover, Rect},
16132            plan_schedule, planner,
16133            proj::{AxisMap, RefProj},
16134            store::{EdgeKey, Tag},
16135        };
16136        let failure = |message: String| {
16137            ExcelError::new(ExcelErrorKind::Error)
16138                .with_message(format!("unified_authority planner: {message}"))
16139        };
16140        self.cancellation_checkpoint("Evaluation cancelled before authority planning")?;
16141        let store = self
16142            .graph
16143            .authority_plan_store()
16144            .map_err(Self::authority_excel_error)?;
16145        // One formula cell without hints (a tiny edit): its plan is the cell
16146        // alone unless it reads itself (`planner::plan_single`).
16147        if let [only] = candidates
16148            && vdeps.is_empty()
16149            && self.graph.authority_host().observed(*only).is_none()
16150            && let Some(cell) = self.graph.authority_cell_of_vertex(*only)
16151            && cell.0 != crate::engine::authority::geom::SYMBOL_SHEET
16152            && let Some(single) = planner::plan_single(store, cell)
16153        {
16154            #[cfg(debug_assertions)]
16155            {
16156                let mut cover = Cover::new();
16157                cover.insert_rect(cell.0, &Rect::new(cell.1, cell.2, cell.1, cell.2));
16158                let general = planner::plan_with_hints(store, &cover, &[], None, None, None)
16159                    .expect("general plan of one cell");
16160                assert_eq!(
16161                    general.cells.as_slice(),
16162                    &[single],
16163                    "single-cell plan differs from the planner at {cell:?}"
16164                );
16165            }
16166            let adapted = plan_schedule::schedule(
16167                &[single],
16168                0,
16169                None,
16170                |cell| {
16171                    self.graph
16172                        .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
16173                        .ok_or_else(|| failure("missing executor identity".to_owned()))
16174                },
16175                |_work| Ok(()),
16176            )
16177            .map_err(|error| match error {
16178                plan_schedule::ScheduleError::Runtime(error) => error,
16179                other => failure(format!("{other:?}")),
16180            })?;
16181            if let Some(ledger) = ledger {
16182                ledger
16183                    .reserve_schedule_discovery(adapted.peak_heap_bytes)
16184                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
16185                ledger
16186                    .release_scratch(adapted.peak_heap_bytes)
16187                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
16188            }
16189            return Ok(adapted.schedule);
16190        }
16191        // A small request of grid formula cells without hints: per-cell
16192        // arcs and longest-path levels (`planner::plan_small`).
16193        if (2..=planner::SMALL_PLAN_MAX).contains(&candidates.len())
16194            && vdeps.is_empty()
16195            && candidates
16196                .iter()
16197                .all(|&v| self.graph.authority_host().observed(v).is_none())
16198        {
16199            let cells: Option<Vec<(u16, u32, u32)>> = candidates
16200                .iter()
16201                .map(|&v| self.graph.authority_cell_of_vertex(v))
16202                .collect();
16203            if let Some(small) = cells.as_deref().and_then(|c| planner::plan_small(store, c)) {
16204                #[cfg(debug_assertions)]
16205                {
16206                    let mut cover = Cover::new();
16207                    for c in cells.as_deref().unwrap_or_default() {
16208                        cover.insert_rect(c.0, &Rect::new(c.1, c.2, c.1, c.2));
16209                    }
16210                    let general = planner::plan_with_hints(store, &cover, &[], None, None, None)
16211                        .expect("general plan of a small request");
16212                    let key = |c: &planner::OrderedCell| (c.sheet, c.row, c.col, c.id, c.owner);
16213                    let mut a: Vec<_> = general.cells.iter().map(key).collect();
16214                    let mut b: Vec<_> = small.iter().map(key).collect();
16215                    a.sort_unstable();
16216                    b.sort_unstable();
16217                    assert_eq!(a, b, "small plan cells differ from the planner");
16218                    assert!(
16219                        general.cells.iter().all(|c| c.cycle.is_none()),
16220                        "small plan of a cyclic request"
16221                    );
16222                }
16223                let adapted = plan_schedule::schedule(
16224                    &small,
16225                    0,
16226                    None,
16227                    |cell| {
16228                        self.graph
16229                            .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
16230                            .ok_or_else(|| failure("missing executor identity".to_owned()))
16231                    },
16232                    |_work| Ok(()),
16233                )
16234                .map_err(|error| match error {
16235                    plan_schedule::ScheduleError::Runtime(error) => error,
16236                    other => failure(format!("{other:?}")),
16237                })?;
16238                if let Some(ledger) = ledger {
16239                    ledger
16240                        .reserve_schedule_discovery(adapted.peak_heap_bytes)
16241                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
16242                    ledger
16243                        .release_scratch(adapted.peak_heap_bytes)
16244                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
16245                }
16246                return Ok(adapted.schedule);
16247            }
16248        }
16249        // Names are symbol-plane nodes (design §4.1): a name vertex plans as
16250        // the unit at its node, between its precedents and its readers.
16251        // Candidates become cells, sorted by (sheet, column, row) and
16252        // coalesced into row intervals: one cover insert per interval.
16253        let mut cover = Cover::new();
16254        {
16255            let cell_of = |&id: &VertexId| {
16256                self.graph
16257                    .authority_cell_of_vertex(id)
16258                    .map(|(sheet, row, col)| (sheet, col, row))
16259            };
16260            // A full recalc maps and sorts every formula: on the pool when
16261            // there is one (first eval's schedule is serial work otherwise).
16262            let cells: Vec<(u16, u32, u32)> = match self.thread_pool.as_deref() {
16263                Some(pool) if candidates.len() >= PARALLEL_SCHEDULE_MIN_CANDIDATES => {
16264                    use rayon::prelude::*;
16265                    pool.install(|| {
16266                        let mut cells: Vec<_> = candidates.par_iter().filter_map(cell_of).collect();
16267                        cells.par_sort_unstable();
16268                        cells
16269                    })
16270                }
16271                _ => {
16272                    let mut cells: Vec<_> = candidates.iter().filter_map(cell_of).collect();
16273                    cells.sort_unstable();
16274                    cells
16275                }
16276            };
16277            let mut i = 0;
16278            while i < cells.len() {
16279                let (sheet, col, r0) = cells[i];
16280                let mut r1 = r0;
16281                let mut j = i + 1;
16282                while j < cells.len() && cells[j].0 == sheet && cells[j].1 == col {
16283                    if cells[j].2 > r1 + 1 {
16284                        break;
16285                    }
16286                    r1 = r1.max(cells[j].2);
16287                    j += 1;
16288                }
16289                cover.insert_rect(sheet, &Rect::new(r0, col, r1, col));
16290                i = j;
16291            }
16292        }
16293        let mut hints = Vec::new();
16294        for (&reader, deps) in vdeps {
16295            let Some(reader) = self.graph.authority_cell_of_vertex(reader) else {
16296                continue;
16297            };
16298            for &dependency in deps {
16299                let Some(dep) = self.graph.authority_cell_of_vertex(dependency) else {
16300                    continue;
16301                };
16302                hints.push(planner::PlanHint {
16303                    reader: (reader.0, reader.2, reader.1),
16304                    edge: EdgeKey {
16305                        dep_sheet: reader.0,
16306                        tag: Tag::X,
16307                        lk: u32::MAX,
16308                        proj: RefProj {
16309                            sheet: dep.0,
16310                            rows: AxisMap::fixed(dep.1, dep.1),
16311                            cols: AxisMap::fixed(dep.2, dep.2),
16312                        },
16313                    },
16314                });
16315            }
16316        }
16317        // rdi_dyn: order each dynamic reader after its observed reads.
16318        let host = self.graph.authority_host();
16319        for &id in candidates.iter().filter(|_| host.has_observed()) {
16320            let Some(reads) = host.observed(id) else {
16321                continue;
16322            };
16323            let Some(reader) = self.graph.authority_cell_of_vertex(id) else {
16324                continue;
16325            };
16326            for &(sheet, r0, c0, r1, c1) in reads {
16327                hints.push(planner::PlanHint {
16328                    reader: (reader.0, reader.2, reader.1),
16329                    edge: EdgeKey {
16330                        dep_sheet: reader.0,
16331                        tag: Tag::X,
16332                        lk: u32::MAX,
16333                        proj: RefProj {
16334                            sheet,
16335                            rows: AxisMap::fixed(r0, r1),
16336                            cols: AxisMap::fixed(c0, c1),
16337                        },
16338                    },
16339                });
16340            }
16341        }
16342        hints.sort_unstable_by_key(|hint| hint.reader);
16343        let checkpoint = ledger
16344            .as_ref()
16345            .map_or(0, |ledger| ledger.scratch_checkpoint());
16346        let scratch_limit = ledger
16347            .as_ref()
16348            .and_then(|ledger| ledger.schedule_discovery_limit())
16349            .map(|limit| limit.saturating_sub(checkpoint));
16350        let ordered = planner::plan_with_hints(store, &cover, &hints, scratch_limit, None, None)
16351            .map_err(|error| failure(format!("{error:?}")))?;
16352        // max_work_units is an execution budget. Planning work must be capped
16353        // independently: charging it here changes the observable publication
16354        // boundary (e.g. a spill must commit before the next execution fails).
16355        let adapted = plan_schedule::schedule(
16356            &ordered.cells,
16357            ordered.heap_bytes(),
16358            scratch_limit,
16359            |cell| {
16360                self.graph
16361                    .authority_vertex_of_formula(cell.id, (cell.sheet, cell.row, cell.col))
16362                    .ok_or_else(|| failure("missing executor identity".to_owned()))
16363            },
16364            |_work| {
16365                self.cancellation_checkpoint("Evaluation cancelled during authority planning")?;
16366                if let Some(ledger) = ledger.as_deref_mut() {
16367                    ledger
16368                        .checkpoint_deadline()
16369                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
16370                }
16371                Ok(())
16372            },
16373        )
16374        .map_err(|error| match error {
16375            plan_schedule::ScheduleError::Runtime(error) => error,
16376            other => failure(format!("{other:?}")),
16377        })?;
16378        if let Some(ledger) = ledger {
16379            let peak = ordered.peak_heap_bytes.max(adapted.peak_heap_bytes);
16380            ledger
16381                .reserve_schedule_discovery(peak)
16382                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
16383            ledger
16384                .release_scratch(peak)
16385                .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
16386        }
16387        // The planner's order is scratch now; a full recalc's is large.
16388        match self.thread_pool.as_deref() {
16389            Some(pool) if ordered.cells.len() >= PARALLEL_SCHEDULE_MIN_CANDIDATES => {
16390                pool.spawn(move || drop(ordered))
16391            }
16392            _ => drop(ordered),
16393        }
16394        Ok(adapted.schedule)
16395    }
16396
16397    /// Static-schedule cache eligibility. Legacy excludes range readers:
16398    /// their order comes from per-request range virtual deps. Under the
16399    /// authority a range read is a static edge of the relation, so only
16400    /// dynamic readers (whose hints are per request) are excluded, and the
16401    /// key adds the authority revision (design §8.4).
16402    fn can_use_static_schedule_cache(&self, to_evaluate: &[VertexId]) -> bool {
16403        {
16404            // A dynamic reader is planned from its observed reads, which the
16405            // key covers (rev.dyn); one without them needs a pre-probe, and
16406            // request-scoped replan hints are never cached.
16407            let host = self.graph.authority_host();
16408            !to_evaluate.is_empty()
16409                && !self.freshness_has_hints()
16410                && to_evaluate
16411                    .iter()
16412                    .all(|&v| !self.graph.is_dynamic(v) || host.observed(v).is_some())
16413        }
16414    }
16415
16416    fn schedule_cache_authority_revision(&self) -> (u64, u64) {
16417        {
16418            // rev.topology is `topology_epoch`; a symbol revision rebuilds the
16419            // store, so it is part of `revision`.
16420            let host = self.graph.authority_host();
16421            (host.revision(), host.rev_dyn())
16422        }
16423    }
16424
16425    fn start_virtual_dep_telemetry(&self) -> VirtualDepTelemetry {
16426        VirtualDepTelemetry {
16427            fallback_mode_activations: self.virtual_dep_fallback_activations,
16428            ..VirtualDepTelemetry::default()
16429        }
16430    }
16431
16432    fn accumulate_schedule_meta(telemetry: &mut VirtualDepTelemetry, meta: &ScheduleBuildMeta) {
16433        telemetry.candidate_vertices_total += meta.candidate_vertices;
16434        telemetry.vdeps_vertices_total += meta.vdeps_vertices;
16435        telemetry.vdeps_edges_total += meta.vdeps_edges;
16436        telemetry.builder_elapsed_ms_total += meta.builder_elapsed_ms;
16437        if meta.schedule_cache_eligible {
16438            if meta.schedule_cache_hit {
16439                telemetry.schedule_cache_hits += 1;
16440                telemetry.reused_schedule_vertices_total += meta.candidate_vertices;
16441            } else {
16442                telemetry.schedule_cache_misses += 1;
16443            }
16444        }
16445        if meta.used_virtual_schedule {
16446            telemetry.schedule_virtual_passes += 1;
16447        } else {
16448            telemetry.schedule_static_passes += 1;
16449        }
16450    }
16451
16452    /// End-of-pass dirty bookkeeping; true when the loop must replan.
16453    /// Legacy: clear the pass, re-dirty readers whose pre-probe changed.
16454    /// Under the authority an armed pass keeps stale and unreached vertices
16455    /// dirty instead (design §8.2, `freshness.rs`).
16456    fn finish_pass_dirty(&mut self, to_evaluate: &[VertexId], changed: &[VertexId]) -> bool {
16457        self.finish_pass_dirty_scoped(to_evaluate, changed, true)
16458    }
16459
16460    /// [`Self::finish_pass_dirty`] for a targeted pass: only its candidates
16461    /// decide whether to replan.
16462    fn finish_target_pass_dirty(&mut self, to_evaluate: &[VertexId], changed: &[VertexId]) -> bool {
16463        self.finish_pass_dirty_scoped(to_evaluate, changed, false)
16464    }
16465
16466    fn finish_pass_dirty_scoped(
16467        &mut self,
16468        to_evaluate: &[VertexId],
16469        changed: &[VertexId],
16470        #[allow(unused_variables)] whole_workbook: bool,
16471    ) -> bool {
16472        self.freshness_finish_pass(to_evaluate, changed, whole_workbook)
16473    }
16474
16475    /// Start a pass over `schedule` (arms the freshness recorder).
16476    fn begin_pass(
16477        &mut self,
16478        #[allow(unused_variables)] schedule: &crate::engine::scheduler::Schedule,
16479    ) {
16480        self.freshness_begin_pass(schedule);
16481    }
16482
16483    /// FR4 layer barrier after unit `index`: true stops the pass.
16484    fn stop_after_unit(
16485        &mut self,
16486        #[allow(unused_variables)] schedule: &crate::engine::scheduler::Schedule,
16487        #[allow(unused_variables)] index: usize,
16488    ) -> bool {
16489        self.freshness_stop_after_unit(schedule, index)
16490    }
16491
16492    fn changed_virtual_dep_vertices(
16493        &mut self,
16494        to_evaluate: &[VertexId],
16495        old_vdeps: &FxHashMap<VertexId, Vec<VertexId>>,
16496    ) -> Vec<VertexId> {
16497        #[cfg(test)]
16498        if self.force_virtual_dep_changes_remaining_for_test > 0
16499            && let Some(vertex) = to_evaluate.first().copied()
16500        {
16501            self.force_virtual_dep_changes_remaining_for_test -= 1;
16502            return vec![vertex];
16503        }
16504        // An armed pass detects stale dynamic reads directly; the pre-probe
16505        // comparison is legacy's substitute for that (design §8.2).
16506        if self.freshness_armed() {
16507            return Vec::new();
16508        }
16509        if !to_evaluate
16510            .iter()
16511            .copied()
16512            .any(|v| self.graph.is_dynamic(v))
16513        {
16514            return Vec::new();
16515        }
16516
16517        let builder = VirtualDepBuilder::new(self);
16518        let (new_vdeps, _) = builder.build(to_evaluate);
16519
16520        let mut candidates = FxHashSet::default();
16521        candidates.extend(old_vdeps.keys().copied());
16522        candidates.extend(new_vdeps.keys().copied());
16523
16524        let mut changed = Vec::new();
16525        for v in candidates {
16526            if old_vdeps.get(&v) != new_vdeps.get(&v) {
16527                changed.push(v);
16528            }
16529        }
16530        changed
16531    }
16532
16533    /// Build a demand-driven subgraph for the given targets, including ephemeral edges for
16534    /// compressed ranges, and returning the set of dirty/volatile precedents and virtual deps.
16535    /// Demand candidates of `targets` and their dynamic plan hints: under
16536    /// `unified_authority` from the authority's relation (design §8.3),
16537    /// otherwise from the legacy graph.
16538    #[allow(clippy::type_complexity)]
16539    fn demand_subgraph(
16540        &self,
16541        targets: &[VertexId],
16542    ) -> Result<
16543        (
16544            Vec<VertexId>,
16545            rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
16546        ),
16547        ExcelError,
16548    > {
16549        self.authority_demand_subgraph(targets)
16550    }
16551
16552    /// Design §8.3: traverse precedents from the targets over the
16553    /// authority's static relation (symbol nodes are ordinary pieces) plus
16554    /// the dynamic readers' virtual dependencies, and collect what legacy's
16555    /// demand walk collects: dirty or volatile formula cells and every name
16556    /// passed through. No legacy dependency structure is read.
16557    #[allow(clippy::type_complexity)]
16558    fn authority_demand_subgraph(
16559        &self,
16560        targets: &[VertexId],
16561    ) -> Result<
16562        (
16563            Vec<VertexId>,
16564            rustc_hash::FxHashMap<VertexId, Vec<VertexId>>,
16565        ),
16566        ExcelError,
16567    > {
16568        use crate::engine::authority::geom::{Cell, Rect, SYMBOL_SHEET};
16569        use crate::engine::authority::store::TagFilter;
16570        use rustc_hash::{FxHashMap, FxHashSet};
16571        let store = self
16572            .graph
16573            .authority_plan_store()
16574            .map_err(Self::authority_excel_error)?;
16575        let ids = store.ids();
16576        let mut to_evaluate: FxHashSet<VertexId> = FxHashSet::default();
16577        let mut vdeps: FxHashMap<VertexId, Vec<VertexId>> = FxHashMap::default();
16578        let mut visited: FxHashSet<Cell> = FxHashSet::default();
16579        // (cell, authority id or NO_VID): the id lets the side array
16580        // translate the cell without a hash lookup.
16581        let mut stack: Vec<(Cell, u32)> = Vec::new();
16582        // Formula cells under `rect` on `sheet`: identity runs per column.
16583        let push_formulas = |stack: &mut Vec<(Cell, u32)>,
16584                             visited: &FxHashSet<Cell>,
16585                             sheet: u16,
16586                             rect: Rect| {
16587            for col in rect.c0..=rect.c1 {
16588                ids.visit_runs_in(sheet, col, rect.r0, rect.r1, &mut |h| {
16589                    let run = ids.run(h);
16590                    let r0 = run.row_start.max(rect.r0);
16591                    let r1 = (run.row_start + run.len - 1).min(rect.r1);
16592                    for row in r0..=r1 {
16593                        if !visited.contains(&(sheet, row, col)) {
16594                            stack.push(((sheet, row, col), run.first_id + (row - run.row_start)));
16595                        }
16596                    }
16597                });
16598            }
16599        };
16600        for &v in targets {
16601            if let Some(table) = self.graph.table_by_vertex(v) {
16602                // A table's demand is its range's, as legacy's table vertex
16603                // leads to the cells it covers (its symbol row has no
16604                // precedents).
16605                let (s, e) = (table.range.start, table.range.end);
16606                push_formulas(
16607                    &mut stack,
16608                    &visited,
16609                    s.sheet_id,
16610                    Rect::new(s.coord.row(), s.coord.col(), e.coord.row(), e.coord.col()),
16611                );
16612            } else if let Some(cell) = self.graph.authority_cell_of_vertex(v) {
16613                stack.push((cell, crate::engine::authority::identity::NO_VID));
16614            }
16615        }
16616        let mut hits = Vec::new();
16617        #[cfg(any(test, feature = "benchmark_internal"))]
16618        let (mut probe_vertices, mut probe_clean_formulas, mut probe_edges, mut probe_dynamic) =
16619            (0, 0, 0, 0);
16620        while let Some((cell, id)) = stack.pop() {
16621            if !visited.insert(cell) {
16622                continue;
16623            }
16624            let Some(v) = self.graph.authority_vertex_of_formula(id, cell) else {
16625                continue;
16626            };
16627            if !self.graph.vertex_exists(v) {
16628                continue;
16629            }
16630            #[cfg(any(test, feature = "benchmark_internal"))]
16631            {
16632                probe_vertices += 1;
16633            }
16634            match self.graph.get_vertex_kind(v) {
16635                VertexKind::FormulaScalar | VertexKind::FormulaArray => {
16636                    if self.graph.is_dirty(v) || self.graph.is_volatile(v) {
16637                        to_evaluate.insert(v);
16638                    } else {
16639                        #[cfg(any(test, feature = "benchmark_internal"))]
16640                        {
16641                            probe_clean_formulas += 1;
16642                        }
16643                    }
16644                }
16645                VertexKind::NamedScalar | VertexKind::NamedArray => {
16646                    to_evaluate.insert(v);
16647                }
16648                _ => {}
16649            }
16650            hits.clear();
16651            store.direct_precedents(cell, TagFilter::All, &mut hits);
16652            #[cfg(any(test, feature = "benchmark_internal"))]
16653            {
16654                probe_edges += hits.len();
16655            }
16656            for &(_, sheet, rect) in &hits {
16657                // DirtyExtents: a dirty spill anchor whose extent meets this
16658                // image is a demand precedent, ordered before `v` (§8.2).
16659                if sheet != SYMBOL_SHEET {
16660                    for anchor in self
16661                        .graph
16662                        .spill_anchors_in_region(sheet, rect.r0, rect.c0, rect.r1, rect.c1)
16663                    {
16664                        if anchor != v && self.graph.is_dirty(anchor) {
16665                            vdeps.entry(v).or_default().push(anchor);
16666                            if let Some(c) = self.graph.authority_cell_of_vertex(anchor) {
16667                                stack.push((c, crate::engine::authority::identity::NO_VID));
16668                            }
16669                        }
16670                    }
16671                }
16672                if sheet == SYMBOL_SHEET {
16673                    stack.extend((rect.r0..=rect.r1).map(|slot| {
16674                        (
16675                            (SYMBOL_SHEET, slot, 0),
16676                            crate::engine::authority::identity::NO_VID,
16677                        )
16678                    }));
16679                    continue;
16680                }
16681                push_formulas(&mut stack, &visited, sheet, rect);
16682            }
16683            if self.graph.is_dynamic(v) {
16684                #[cfg(any(test, feature = "benchmark_internal"))]
16685                {
16686                    probe_dynamic += 1;
16687                }
16688                // rdi_dyn: the observed reads are demand precedents.
16689                if let Some(reads) = self.graph.authority_host().observed(v) {
16690                    for &(sheet, r0, c0, r1, c1) in reads {
16691                        push_formulas(&mut stack, &visited, sheet, Rect::new(r0, c0, r1, c1));
16692                    }
16693                }
16694                let (vdeps_map, _) = VirtualDepBuilder::new(self).build(&[v]);
16695                // Pre-probe targets plus reads this request found dirty
16696                // (design §8.3: demand walks rdi ∪ hints).
16697                let hinted = self.freshness_hints(v).unwrap_or(&[]);
16698                if let Some(deps) = vdeps_map
16699                    .get(&v)
16700                    .map(|deps| deps.iter().chain(hinted))
16701                    .or(Some([].iter().chain(hinted)))
16702                {
16703                    for &u in deps {
16704                        vdeps.entry(v).or_default().push(u);
16705                        if let Some(c) = self.graph.authority_cell_of_vertex(u) {
16706                            stack.push((c, crate::engine::authority::identity::NO_VID));
16707                        }
16708                    }
16709                }
16710            }
16711        }
16712        let mut result: Vec<VertexId> = to_evaluate.into_iter().collect();
16713        result.sort_unstable();
16714        for deps in vdeps.values_mut() {
16715            deps.sort_unstable();
16716            deps.dedup();
16717        }
16718        #[cfg(any(test, feature = "benchmark_internal"))]
16719        {
16720            let mut probe = self.recalc_reuse_probe.lock().unwrap();
16721            probe.demand_builds += 1;
16722            probe.demand_vertices += probe_vertices;
16723            probe.demand_clean_formulas += probe_clean_formulas;
16724            probe.demand_explicit_edges += probe_edges;
16725            probe.demand_virtual_builder_calls += probe_dynamic;
16726        }
16727        Ok((result, vdeps))
16728    }
16729
16730    /// Helper: convert 1-based column index to Excel-style letters (1 -> A, 27 -> AA)
16731    fn col_to_letters(col: u32) -> String {
16732        col_letters_from_1based(col).expect("column index must be >= 1")
16733    }
16734
16735    /// Evaluate all dirty/volatile vertices with cancellation support
16736    pub fn evaluate_all_cancellable(
16737        &mut self,
16738        cancel: crate::engine::CancelToken,
16739    ) -> Result<EvalResult, ExcelError> {
16740        self.observe_evaluation_resource_request(EvaluationRequestKind::FullCancellable, |engine| {
16741            engine.observe_function_semantic_epoch()?;
16742            engine.active_cancel_flag = Some(cancel.clone());
16743            let res = engine.evaluate_all_cancellable_impl(cancel.as_flag());
16744            engine.active_cancel_flag = None;
16745            res
16746        })
16747    }
16748
16749    fn evaluate_all_cancellable_impl(
16750        &mut self,
16751        cancel_flag: &AtomicBool,
16752    ) -> Result<EvalResult, ExcelError> {
16753        let _source_cache = self.source_cache_session();
16754        self.validate_deterministic_mode()?;
16755        if self.config.defer_graph_building {
16756            self.build_graph_all()?;
16757        }
16758        if cancel_flag.load(Ordering::Relaxed) {
16759            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16760                .with_message("Evaluation cancelled before scheduling".to_string()));
16761        }
16762        self.require_unified_authority()?;
16763        self.begin_evaluation_request();
16764        self.reset_virtual_dep_telemetry_if_disabled();
16765        let start = crate::instant::FzInstant::now();
16766        let mut computed_vertices = 0;
16767        let mut cycle_errors = 0;
16768
16769        let mut replan_iterations = 0;
16770        const MAX_REPLAN: usize = 5;
16771        let mut telemetry = self
16772            .config
16773            .enable_virtual_dep_telemetry
16774            .then(|| self.start_virtual_dep_telemetry());
16775
16776        loop {
16777            if cancel_flag.load(Ordering::Relaxed) {
16778                if let Some(mut t) = telemetry {
16779                    t.bailout_reason = Some("cancelled");
16780                    t.replan_iterations = replan_iterations;
16781                    self.last_virtual_dep_telemetry = t;
16782                }
16783                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16784                    .with_message("Evaluation cancelled before scheduling".to_string()));
16785            }
16786
16787            let to_evaluate = self.graph.get_evaluation_vertices();
16788            if to_evaluate.is_empty() {
16789                if let Some(t) = telemetry.as_mut()
16790                    && t.bailout_reason.is_none()
16791                {
16792                    t.bailout_reason = Some("no_work");
16793                }
16794                break;
16795            }
16796
16797            let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
16798            if let Some(t) = telemetry.as_mut() {
16799                Self::accumulate_schedule_meta(t, &meta);
16800            }
16801
16802            // Walk units in condensation order, checking cancellation between
16803            // units (formerly between cycles and between layers).
16804            self.begin_pass(&schedule);
16805            for (unit_index, &unit) in schedule.units.iter().enumerate() {
16806                match unit {
16807                    ScheduleUnit::Cycle(i) => {
16808                        // Check cancellation between cycles
16809                        if cancel_flag.load(Ordering::Relaxed) {
16810                            if let Some(mut t) = telemetry {
16811                                t.bailout_reason = Some("cancelled");
16812                                t.replan_iterations = replan_iterations;
16813                                self.last_virtual_dep_telemetry = t;
16814                            }
16815                            return Err(ExcelError::new(ExcelErrorKind::Cancelled).with_message(
16816                                "Evaluation cancelled during cycle handling".to_string(),
16817                            ));
16818                        }
16819
16820                        if self.handle_cycle_unit(
16821                            schedule.unit_cycle(i),
16822                            None,
16823                            None,
16824                            Some(cancel_flag),
16825                        )? > 0
16826                        {
16827                            cycle_errors += 1;
16828                        }
16829                    }
16830                    ScheduleUnit::Layer(i) => {
16831                        let layer = schedule.unit_layer(i);
16832                        // Check cancellation between layers
16833                        if cancel_flag.load(Ordering::Relaxed) {
16834                            if let Some(mut t) = telemetry {
16835                                t.bailout_reason = Some("cancelled");
16836                                t.replan_iterations = replan_iterations;
16837                                self.last_virtual_dep_telemetry = t;
16838                            }
16839                            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16840                                .with_message("Evaluation cancelled between layers".to_string()));
16841                        }
16842
16843                        // Evaluate vertices in this layer (parallel or sequential)
16844                        if self.thread_pool.is_some() && layer.vertices.len() > 1 {
16845                            computed_vertices +=
16846                                self.evaluate_layer_parallel_cancellable(layer, cancel_flag)?;
16847                        } else {
16848                            computed_vertices +=
16849                                self.evaluate_layer_sequential_cancellable(layer, cancel_flag)?;
16850                        }
16851                    }
16852                }
16853                if self.stop_after_unit(&schedule, unit_index) {
16854                    break;
16855                }
16856            }
16857
16858            let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
16859            if let Some(t) = telemetry.as_mut() {
16860                t.changed_vdeps_total += changed_vertices.len();
16861            }
16862            self.resource_checkpoint(0)?;
16863            if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
16864                if let Some(t) = telemetry.as_mut() {
16865                    t.bailout_reason = Some("converged");
16866                }
16867                break;
16868            }
16869            if replan_iterations >= MAX_REPLAN {
16870                if let Some(mut t) = telemetry.take() {
16871                    t.bailout_reason = Some("max_replan");
16872                    t.replan_iterations = replan_iterations;
16873                    self.last_virtual_dep_telemetry = t;
16874                }
16875                return Err(
16876                    self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
16877                );
16878            }
16879            replan_iterations += 1;
16880        }
16881
16882        if let Some(mut t) = telemetry {
16883            t.replan_iterations = replan_iterations;
16884            self.last_virtual_dep_telemetry = t;
16885        }
16886
16887        // Re-dirty volatile vertices for the next evaluation cycle
16888        self.redirty_for_next_recalc();
16889        self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
16890
16891        Ok(EvalResult {
16892            computed_vertices,
16893            cycle_errors,
16894            elapsed: start.elapsed(),
16895        })
16896    }
16897
16898    /// Evaluate only the necessary precedents for specific target cells with cancellation support
16899    pub fn evaluate_until_cancellable(
16900        &mut self,
16901        targets: &[&str],
16902        cancel: crate::engine::CancelToken,
16903    ) -> Result<EvalResult, ExcelError> {
16904        self.observe_evaluation_resource_request(
16905            EvaluationRequestKind::TargetedCancellable,
16906            |engine| {
16907                engine.observe_function_semantic_epoch()?;
16908                engine.active_cancel_flag = Some(cancel.clone());
16909                let res = engine.evaluate_until_cancellable_impl(targets, cancel.as_flag());
16910                engine.active_cancel_flag = None;
16911                res
16912            },
16913        )
16914    }
16915
16916    fn evaluate_until_cancellable_impl(
16917        &mut self,
16918        targets: &[&str],
16919        cancel_flag: &AtomicBool,
16920    ) -> Result<EvalResult, ExcelError> {
16921        if cancel_flag.load(Ordering::Relaxed) {
16922            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
16923                .with_message("Evaluation cancelled before target preparation"));
16924        }
16925        let mut typed_targets = Vec::with_capacity(targets.len());
16926        for target in targets {
16927            let (sheet, row, col) = self.parse_a1_notation(target)?;
16928            self.graph.sheet_id_mut(&sheet);
16929            typed_targets.push(crate::engine::EvaluationTarget::Cell { sheet, row, col });
16930        }
16931        self.evaluate_mixed_targets(&typed_targets, None)
16932    }
16933
16934    fn parse_a1_notation(&self, address: &str) -> Result<(String, u32, u32), ExcelError> {
16935        let mut quoted = false;
16936        let mut separator = None;
16937        let bytes = address.as_bytes();
16938        let mut index = 0usize;
16939        while index < bytes.len() {
16940            match bytes[index] {
16941                b'\'' => {
16942                    if quoted && bytes.get(index + 1) == Some(&b'\'') {
16943                        index = index.saturating_add(1);
16944                    } else {
16945                        quoted = !quoted;
16946                    }
16947                }
16948                b'!' if !quoted => separator = Some(index),
16949                _ => {}
16950            }
16951            index = index.saturating_add(1);
16952        }
16953        if quoted {
16954            return Err(ExcelError::new(ExcelErrorKind::Ref)
16955                .with_message(format!("Invalid quoted sheet reference `{address}`")));
16956        }
16957        let (sheet, cell_part) = match separator {
16958            Some(separator) => {
16959                let raw_sheet = &address[..separator];
16960                let sheet = if raw_sheet.starts_with('\'') && raw_sheet.ends_with('\'') {
16961                    raw_sheet[1..raw_sheet.len().saturating_sub(1)].replace("''", "'")
16962                } else {
16963                    raw_sheet.to_string()
16964                };
16965                (sheet, &address[separator + 1..])
16966            }
16967            None => (self.default_sheet_name().to_string(), address),
16968        };
16969
16970        let (row, col, _, _) = parse_a1_1based(cell_part).map_err(|err| {
16971            ExcelError::new(ExcelErrorKind::Ref)
16972                .with_message(format!("Invalid cell reference `{cell_part}`: {err}"))
16973        })?;
16974
16975        Ok((sheet, row, col))
16976    }
16977
16978    /// Determine volatility using this engine's FunctionProvider, falling back to global registry.
16979    fn is_ast_volatile_with_provider(&self, ast: &ASTNode) -> bool {
16980        use formualizer_parse::parser::ASTNodeType;
16981        match &ast.node_type {
16982            ASTNodeType::Function { name, args, .. } => {
16983                if let Some(func) = self
16984                    .get_function("", name)
16985                    .or_else(|| crate::function_registry::get("", name))
16986                    && func.caps().contains(crate::function::FnCaps::VOLATILE)
16987                {
16988                    return true;
16989                }
16990                args.iter()
16991                    .any(|arg| self.is_ast_volatile_with_provider(arg))
16992            }
16993            ASTNodeType::BinaryOp { left, right, .. } => {
16994                self.is_ast_volatile_with_provider(left)
16995                    || self.is_ast_volatile_with_provider(right)
16996            }
16997            ASTNodeType::UnaryOp { expr, .. } => self.is_ast_volatile_with_provider(expr),
16998            ASTNodeType::Array(rows) => rows.iter().any(|row| {
16999                row.iter()
17000                    .any(|cell| self.is_ast_volatile_with_provider(cell))
17001            }),
17002            _ => false,
17003        }
17004    }
17005
17006    /// Evaluate a layer sequentially
17007    fn evaluate_layer_sequential(
17008        &mut self,
17009        layer: &super::scheduler::Layer,
17010    ) -> Result<usize, ExcelError> {
17011        self.resource_checkpoint(layer.vertices.len() as u64)?;
17012        self.evaluate_layer_sequential_effects(layer)
17013    }
17014
17015    fn update_vertex_value_with_delta(
17016        &mut self,
17017        vertex_id: VertexId,
17018        new_value: LiteralValue,
17019        delta: &mut DeltaCollector,
17020    ) {
17021        if delta.mode != DeltaMode::Off
17022            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
17023        {
17024            let sheet_name = self.graph.sheet_name(cell.sheet_id);
17025            let old = self
17026                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
17027                .unwrap_or(LiteralValue::Empty);
17028            if old != new_value {
17029                delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
17030            }
17031        }
17032        self.graph.update_vertex_value_ref(vertex_id, &new_value);
17033        self.mirror_vertex_value_to_overlay(vertex_id, &new_value);
17034    }
17035
17036    fn evaluate_layer_sequential_with_delta(
17037        &mut self,
17038        layer: &super::scheduler::Layer,
17039        delta: &mut DeltaCollector,
17040    ) -> Result<usize, ExcelError> {
17041        self.resource_checkpoint(layer.vertices.len() as u64)?;
17042        self.evaluate_layer_sequential_with_delta_effects(layer, delta)
17043    }
17044
17045    /// Evaluate a layer sequentially with cancellation support
17046    fn evaluate_layer_sequential_cancellable(
17047        &mut self,
17048        layer: &super::scheduler::Layer,
17049        cancel_flag: &AtomicBool,
17050    ) -> Result<usize, ExcelError> {
17051        self.resource_checkpoint(layer.vertices.len() as u64)?;
17052        self.evaluate_layer_sequential_cancellable_effects(layer, cancel_flag)
17053    }
17054
17055    /// Evaluate a layer sequentially with more frequent cancellation checks for demand-driven evaluation
17056    fn evaluate_layer_sequential_cancellable_demand_driven(
17057        &mut self,
17058        layer: &super::scheduler::Layer,
17059        cancel_flag: &AtomicBool,
17060    ) -> Result<usize, ExcelError> {
17061        self.resource_checkpoint(layer.vertices.len() as u64)?;
17062        self.evaluate_layer_sequential_cancellable_demand_driven_effects(layer, cancel_flag)
17063    }
17064
17065    /// Evaluate a layer in parallel using the thread pool.
17066    ///
17067    /// Cost-adaptive: the layer starts sequentially in slices of doubling
17068    /// size (1, 2, 4, ... vertices, capped so a slice does not overshoot the
17069    /// probe) and hands the rest to the pool once the rest looks worth it
17070    /// (`PARALLEL_LAYER_WORTH` at the rate so far) or the probe
17071    /// (`PARALLEL_LAYER_PROBE`) is spent. A cheap layer never pays the pool's
17072    /// wake-up and join (most Enron layers are tens of µs of work); an
17073    /// expensive one goes parallel after a few vertices. Splitting a layer
17074    /// into consecutive sub-layers is a valid order: its vertices are
17075    /// independent.
17076    fn evaluate_layer_parallel(
17077        &mut self,
17078        layer: &super::scheduler::Layer,
17079    ) -> Result<usize, ExcelError> {
17080        if layer.sequential {
17081            return self.evaluate_layer_sequential(layer);
17082        }
17083        self.resource_checkpoint(layer.vertices.len() as u64)?;
17084        let len = layer.vertices.len();
17085        let buffered = buffer_layer_writes(layer);
17086        let (probe, worth) = (PARALLEL_LAYER_PROBE, PARALLEL_LAYER_WORTH);
17087        let start = crate::instant::FzInstant::now();
17088        let mut pos = 0usize;
17089        let mut step = 1usize;
17090        while pos < len {
17091            if pos > 0 {
17092                let elapsed = start.elapsed();
17093                // Rate so far (ns per vertex) and the rest at that rate.
17094                let per_vertex = elapsed.as_nanos() / pos as u128 + 1;
17095                let rest_estimate = per_vertex * (len - pos) as u128;
17096                if len - pos >= 2 && (elapsed >= probe || rest_estimate >= worth.as_nanos()) {
17097                    let rest = layer.sub_layer(pos, len);
17098                    // Expensive members (a SUMIF over a table) parallelize
17099                    // one per task; cheap ones keep runs of 8 together.
17100                    let min_chunk = if per_vertex >= EXPENSIVE_VERTEX_NS {
17101                        1
17102                    } else {
17103                        8
17104                    };
17105                    return Ok(pos + self.evaluate_layer_parallel_effects(&rest, min_chunk)?);
17106                }
17107                // Next slice: double, but no more than the rest of the probe
17108                // at the rate so far (a slice must not overshoot it).
17109                let fit = (probe.saturating_sub(elapsed).as_nanos() / per_vertex) as usize + 1;
17110                step = step.saturating_mul(2).min(fit);
17111            }
17112            let end = (pos + step).min(len);
17113            let slice = layer.sub_layer(pos, end);
17114            // A slice stops at the probe's end even if its members turn out
17115            // far more expensive than the rate so far predicted.
17116            pos += self.evaluate_layer_units_until(
17117                &slice,
17118                None,
17119                None,
17120                None,
17121                buffered,
17122                Some(start + probe),
17123            )?;
17124        }
17125        Ok(len)
17126    }
17127
17128    fn evaluate_layer_parallel_with_delta(
17129        &mut self,
17130        layer: &super::scheduler::Layer,
17131        delta: &mut DeltaCollector,
17132    ) -> Result<usize, ExcelError> {
17133        if layer.sequential {
17134            return self.evaluate_layer_sequential_with_delta(layer, delta);
17135        }
17136        self.resource_checkpoint(layer.vertices.len() as u64)?;
17137        self.evaluate_layer_parallel_with_delta_effects(layer, delta)
17138    }
17139
17140    /// Evaluate a layer in parallel with cancellation support
17141    fn evaluate_layer_parallel_cancellable(
17142        &mut self,
17143        layer: &super::scheduler::Layer,
17144        cancel_flag: &AtomicBool,
17145    ) -> Result<usize, ExcelError> {
17146        if layer.sequential {
17147            return self.evaluate_layer_sequential_cancellable(layer, cancel_flag);
17148        }
17149        self.resource_checkpoint(layer.vertices.len() as u64)?;
17150        self.evaluate_layer_parallel_cancellable_effects(layer, cancel_flag)
17151    }
17152
17153    /// Evaluate a single vertex without mutating the graph (for parallel evaluation)
17154    fn evaluate_vertex_immutable(&self, vertex_id: VertexId) -> Result<LiteralValue, ExcelError> {
17155        // Check if vertex exists
17156        if !self.graph.vertex_exists(vertex_id) {
17157            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
17158                .with_message(format!("Vertex not found: {vertex_id:?}")));
17159        }
17160
17161        // Get vertex kind and check if it needs evaluation
17162        let kind = self.graph.get_vertex_kind(vertex_id);
17163        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
17164
17165        let view = match kind {
17166            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
17167                if let Some(view) = self.graph.formula_view(vertex_id) {
17168                    view
17169                } else {
17170                    return Ok(LiteralValue::Number(0.0));
17171                }
17172            }
17173            VertexKind::Empty | VertexKind::Cell => {
17174                if let Some(cell_ref) = self.graph.get_cell_ref(vertex_id) {
17175                    let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
17176                    let row = cell_ref.coord.row() + 1;
17177                    let col = cell_ref.coord.col() + 1;
17178                    if let Some(v) = self.read_cell_value(sheet_name, row, col) {
17179                        return Ok(v);
17180                    }
17181                }
17182                return Ok(LiteralValue::Number(0.0));
17183            }
17184            VertexKind::NamedScalar => {
17185                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
17186                    ExcelError::new(ExcelErrorKind::Name)
17187                        .with_message("Named range metadata missing".to_string())
17188                })?;
17189
17190                return match &named_range.definition {
17191                    NamedDefinition::Cell(cell_ref) => {
17192                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
17193                        Ok(self
17194                            .get_cell_value(
17195                                sheet_name,
17196                                cell_ref.coord.row() + 1,
17197                                cell_ref.coord.col() + 1,
17198                            )
17199                            .unwrap_or(LiteralValue::Empty))
17200                    }
17201                    NamedDefinition::Literal(v) => Ok(v.clone()),
17202                    NamedDefinition::Formula { ast, .. } => {
17203                        let context_sheet = match named_range.scope {
17204                            NameScope::Sheet(id) => id,
17205                            NameScope::Workbook => sheet_id,
17206                        };
17207                        let sheet_name = self.graph.sheet_name(context_sheet);
17208                        let cell_ref = self
17209                            .graph
17210                            .get_cell_ref(vertex_id)
17211                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
17212                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
17213                        interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
17214                    }
17215                    NamedDefinition::Range(_) => Err(ExcelError::new(ExcelErrorKind::Value)
17216                        .with_message("Range-valued name evaluated as scalar".to_string())),
17217                };
17218            }
17219            VertexKind::NamedArray => {
17220                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
17221                    ExcelError::new(ExcelErrorKind::Name)
17222                        .with_message("Named range metadata missing".to_string())
17223                })?;
17224
17225                return match &named_range.definition {
17226                    NamedDefinition::Range(range_ref) => {
17227                        if range_ref.start.sheet_id != range_ref.end.sheet_id {
17228                            return Err(ExcelError::new(ExcelErrorKind::Ref)
17229                                .with_message("Named range cannot span sheets".to_string()));
17230                        }
17231                        let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
17232                        let sr0 = range_ref.start.coord.row();
17233                        let sc0 = range_ref.start.coord.col();
17234                        let er0 = range_ref.end.coord.row();
17235                        let ec0 = range_ref.end.coord.col();
17236                        if sr0 > er0 || sc0 > ec0 {
17237                            return Err(ExcelError::new(ExcelErrorKind::Ref)
17238                                .with_message("Invalid named range bounds".to_string()));
17239                        }
17240
17241                        let h = (er0 - sr0 + 1) as usize;
17242                        let w = (ec0 - sc0 + 1) as usize;
17243                        let cell_count = (h as u64).saturating_mul(w as u64);
17244                        if cell_count > self.config.spill.max_spill_cells as u64 {
17245                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
17246                                "Named range too large to materialize as an array".to_string(),
17247                            ));
17248                        }
17249
17250                        // `get_cell_value` per cell, with the sheet resolved
17251                        // once (`read_cell_formatted_in` is its body).
17252                        let sheet_id = range_ref.start.sheet_id;
17253                        let asheet = self.arrow_sheets.sheet(sheet_name);
17254                        let mut rows = Vec::with_capacity(h);
17255                        for r0 in sr0..=er0 {
17256                            let mut row = Vec::with_capacity(w);
17257                            for c0 in sc0..=ec0 {
17258                                row.push(
17259                                    self.read_cell_formatted_in(sheet_id, asheet, r0 + 1, c0 + 1)
17260                                        .0,
17261                                );
17262                            }
17263                            rows.push(row);
17264                        }
17265                        Ok(LiteralValue::Array(rows))
17266                    }
17267                    NamedDefinition::Cell(cell_ref) => {
17268                        let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
17269                        let row = cell_ref.coord.row() + 1;
17270                        let col = cell_ref.coord.col() + 1;
17271                        let v = self
17272                            .get_cell_value(sheet_name, row, col)
17273                            .unwrap_or(LiteralValue::Empty);
17274                        Ok(LiteralValue::Array(vec![vec![v]]))
17275                    }
17276                    NamedDefinition::Literal(v) => Ok(LiteralValue::Array(vec![vec![v.clone()]])),
17277                    NamedDefinition::Formula { ast, .. } => {
17278                        let context_sheet = match named_range.scope {
17279                            NameScope::Sheet(id) => id,
17280                            NameScope::Workbook => sheet_id,
17281                        };
17282                        let sheet_name = self.graph.sheet_name(context_sheet);
17283                        let cell_ref = self
17284                            .graph
17285                            .get_cell_ref(vertex_id)
17286                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
17287                        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
17288                        match interpreter.evaluate_ast(ast) {
17289                            Ok(cv) => {
17290                                let v = cv.into_literal();
17291                                match v {
17292                                    LiteralValue::Array(_) => Ok(v),
17293                                    other => Ok(LiteralValue::Array(vec![vec![other]])),
17294                                }
17295                            }
17296                            Err(err) => Ok(LiteralValue::Error(err)),
17297                        }
17298                    }
17299                };
17300            }
17301            VertexKind::InfiniteRange
17302            | VertexKind::Range
17303            | VertexKind::External
17304            | VertexKind::Table => {
17305                // Not directly evaluatable here.
17306                return Ok(LiteralValue::Number(0.0));
17307            }
17308        };
17309
17310        // The interpreter uses a reference to the engine as the context
17311        let sheet_name = self.graph.sheet_name(sheet_id);
17312        let cell_ref = self
17313            .graph
17314            .get_cell_ref(vertex_id)
17315            .expect("cell ref for vertex");
17316        if let Some(result) =
17317            self.freshness_evaluate_recorded(vertex_id, sheet_name, cell_ref, view)
17318        {
17319            return result;
17320        }
17321        let interpreter = Interpreter::new_with_cell(self, sheet_name, cell_ref);
17322
17323        interpreter
17324            .evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
17325            .map(|cv| {
17326                let format = cv.format_id();
17327                self.record_derived_format(vertex_id, format);
17328                self.materialize_formula_result(vertex_id, cv)
17329            })
17330            .or_else(|error| self.fit_formula_error(vertex_id, error))
17331    }
17332
17333    /// Get access to the shared thread pool for parallel evaluation
17334    pub fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
17335        self.thread_pool.as_ref()
17336    }
17337}
17338
17339#[derive(Default)]
17340struct RowBoundsCache {
17341    snapshot: u64,
17342    // key: (sheet_id, col_idx)
17343    map: rustc_hash::FxHashMap<(u32, usize), (Option<u32>, Option<u32>)>,
17344}
17345
17346impl RowBoundsCache {
17347    fn new(snapshot: u64) -> Self {
17348        Self {
17349            snapshot,
17350            map: Default::default(),
17351        }
17352    }
17353    fn get_row_bounds(
17354        &self,
17355        sheet_id: SheetId,
17356        col_idx: usize,
17357        snapshot: u64,
17358    ) -> Option<(Option<u32>, Option<u32>)> {
17359        if self.snapshot != snapshot {
17360            return None;
17361        }
17362        self.map.get(&(sheet_id as u32, col_idx)).copied()
17363    }
17364    fn put_row_bounds(
17365        &mut self,
17366        sheet_id: SheetId,
17367        col_idx: usize,
17368        snapshot: u64,
17369        bounds: (Option<u32>, Option<u32>),
17370    ) {
17371        if self.snapshot != snapshot {
17372            self.snapshot = snapshot;
17373            self.map.clear();
17374        }
17375        self.map.insert((sheet_id as u32, col_idx), bounds);
17376    }
17377}
17378
17379struct UsedAxisBoundsCache {
17380    snapshot: u64,
17381    row_bounds_by_col_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
17382    col_bounds_by_row_span: rustc_hash::FxHashMap<(SheetId, u32, u32), Option<(u32, u32)>>,
17383    #[cfg(test)]
17384    row_hits: std::sync::atomic::AtomicUsize,
17385    #[cfg(test)]
17386    row_misses: std::sync::atomic::AtomicUsize,
17387    #[cfg(test)]
17388    col_hits: std::sync::atomic::AtomicUsize,
17389    #[cfg(test)]
17390    col_misses: std::sync::atomic::AtomicUsize,
17391}
17392
17393impl UsedAxisBoundsCache {
17394    fn new(snapshot: u64) -> Self {
17395        Self {
17396            snapshot,
17397            row_bounds_by_col_span: Default::default(),
17398            col_bounds_by_row_span: Default::default(),
17399            #[cfg(test)]
17400            row_hits: std::sync::atomic::AtomicUsize::new(0),
17401            #[cfg(test)]
17402            row_misses: std::sync::atomic::AtomicUsize::new(0),
17403            #[cfg(test)]
17404            col_hits: std::sync::atomic::AtomicUsize::new(0),
17405            #[cfg(test)]
17406            col_misses: std::sync::atomic::AtomicUsize::new(0),
17407        }
17408    }
17409
17410    fn reset_for_snapshot(&mut self, snapshot: u64) {
17411        if self.snapshot != snapshot {
17412            self.snapshot = snapshot;
17413            self.row_bounds_by_col_span.clear();
17414            self.col_bounds_by_row_span.clear();
17415        }
17416    }
17417
17418    fn get_row_bounds(
17419        &self,
17420        sheet_id: SheetId,
17421        start_col: u32,
17422        end_col: u32,
17423        snapshot: u64,
17424    ) -> Option<Option<(u32, u32)>> {
17425        if self.snapshot != snapshot {
17426            return None;
17427        }
17428        let cached = self
17429            .row_bounds_by_col_span
17430            .get(&(sheet_id, start_col, end_col))
17431            .copied();
17432        #[cfg(test)]
17433        if cached.is_some() {
17434            self.row_hits.fetch_add(1, Ordering::Relaxed);
17435        }
17436        cached
17437    }
17438
17439    fn put_row_bounds(
17440        &mut self,
17441        sheet_id: SheetId,
17442        start_col: u32,
17443        end_col: u32,
17444        snapshot: u64,
17445        bounds: Option<(u32, u32)>,
17446    ) {
17447        self.reset_for_snapshot(snapshot);
17448        self.row_bounds_by_col_span
17449            .insert((sheet_id, start_col, end_col), bounds);
17450        #[cfg(test)]
17451        self.row_misses.fetch_add(1, Ordering::Relaxed);
17452    }
17453
17454    fn get_col_bounds(
17455        &self,
17456        sheet_id: SheetId,
17457        start_row: u32,
17458        end_row: u32,
17459        snapshot: u64,
17460    ) -> Option<Option<(u32, u32)>> {
17461        if self.snapshot != snapshot {
17462            return None;
17463        }
17464        let cached = self
17465            .col_bounds_by_row_span
17466            .get(&(sheet_id, start_row, end_row))
17467            .copied();
17468        #[cfg(test)]
17469        if cached.is_some() {
17470            self.col_hits.fetch_add(1, Ordering::Relaxed);
17471        }
17472        cached
17473    }
17474
17475    fn put_col_bounds(
17476        &mut self,
17477        sheet_id: SheetId,
17478        start_row: u32,
17479        end_row: u32,
17480        snapshot: u64,
17481        bounds: Option<(u32, u32)>,
17482    ) {
17483        self.reset_for_snapshot(snapshot);
17484        self.col_bounds_by_row_span
17485            .insert((sheet_id, start_row, end_row), bounds);
17486        #[cfg(test)]
17487        self.col_misses.fetch_add(1, Ordering::Relaxed);
17488    }
17489}
17490
17491// Phase 2 shim: in-process spill manager delegating to current graph methods.
17492#[derive(Default)]
17493pub struct ShimSpillManager {
17494    region_locks: RegionLockManager,
17495    pub(crate) active_locks: rustc_hash::FxHashMap<VertexId, u64>,
17496}
17497
17498impl ShimSpillManager {
17499    pub(crate) fn reserve(
17500        &mut self,
17501        owner: VertexId,
17502        anchor_cell: CellRef,
17503        shape: SpillShape,
17504        _meta: SpillMeta,
17505    ) -> Result<(), ExcelError> {
17506        // Derive region from anchor + shape; enforce in-flight exclusivity only.
17507        let region = crate::engine::spill::Region {
17508            sheet_id: anchor_cell.sheet_id as u32,
17509            row_start: anchor_cell.coord.row(),
17510            row_end: anchor_cell
17511                .coord
17512                .row()
17513                .saturating_add(shape.rows)
17514                .saturating_sub(1),
17515            col_start: anchor_cell.coord.col(),
17516            col_end: anchor_cell
17517                .coord
17518                .col()
17519                .saturating_add(shape.cols)
17520                .saturating_sub(1),
17521        };
17522        match self.region_locks.reserve(region, owner) {
17523            Ok(id) => {
17524                if id != 0 {
17525                    self.active_locks.insert(owner, id);
17526                }
17527                Ok(())
17528            }
17529            Err(e) => Err(e),
17530        }
17531    }
17532
17533    /// Release any in-flight region reservation still held for `owner`.
17534    ///
17535    /// Reservations are normally released on commit/rollback, but if an anchor is
17536    /// abandoned without committing (e.g. cycle detection stamps it with #CIRC), a
17537    /// stale reservation could remain. This is a no-op when nothing is held.
17538    pub(crate) fn release_owner(&mut self, owner: VertexId) {
17539        if let Some(id) = self.active_locks.remove(&owner) {
17540            self.region_locks.release(id);
17541        }
17542    }
17543
17544    pub(crate) fn commit_array_with_value_probe<F>(
17545        &mut self,
17546        graph: &mut DependencyGraph,
17547        anchor_vertex: VertexId,
17548        targets: &[CellRef],
17549        rows: Vec<Vec<LiteralValue>>,
17550        mut value_probe: F,
17551    ) -> Result<(), ExcelError>
17552    where
17553        F: FnMut(&DependencyGraph, &CellRef) -> Option<LiteralValue>,
17554    {
17555        use formualizer_common::{ExcelErrorExtra, ExcelErrorKind};
17556
17557        // Re-run plan on concrete targets before committing to respect blockers.
17558        // This plan checks formula/spill ownership in the graph, but when the graph value cache
17559        // is disabled (Arrow-canonical mode), it cannot see non-empty value blockers.
17560        let plan_res = graph.plan_spill_region(anchor_vertex, targets);
17561        if let Err(e) = plan_res {
17562            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17563                self.region_locks.release(id);
17564            }
17565            return Err(e);
17566        }
17567
17568        if !graph.value_cache_enabled() {
17569            // Compute expected spill shape from the target rectangle for diagnostics.
17570            let (expected_rows, expected_cols) = if targets.is_empty() {
17571                (0u32, 0u32)
17572            } else {
17573                let mut min_r = u32::MAX;
17574                let mut max_r = 0u32;
17575                let mut min_c = u32::MAX;
17576                let mut max_c = 0u32;
17577                for cell in targets {
17578                    let r = cell.coord.row();
17579                    let c = cell.coord.col();
17580                    min_r = min_r.min(r);
17581                    max_r = max_r.max(r);
17582                    min_c = min_c.min(c);
17583                    max_c = max_c.max(c);
17584                }
17585                (
17586                    max_r.saturating_sub(min_r).saturating_add(1),
17587                    max_c.saturating_sub(min_c).saturating_add(1),
17588                )
17589            };
17590
17591            let anchor_cell = graph
17592                .get_cell_ref(anchor_vertex)
17593                .expect("anchor cell ref for spill commit");
17594
17595            for cell in targets {
17596                // Never treat the anchor as a blocker.
17597                if *cell == anchor_cell {
17598                    continue;
17599                }
17600                // Skip cells already known to be owned by a spill; plan() handled spill conflicts.
17601                if graph.spill_registry_anchor_for_cell(*cell).is_some() {
17602                    continue;
17603                }
17604                // Skip formula vertices in the target region; plan() handled them (or allowed).
17605                if let Some(vid) = graph.get_vertex_id_for_address(cell)
17606                    && vid != anchor_vertex
17607                {
17608                    match graph.get_vertex_kind(vid) {
17609                        crate::engine::vertex::VertexKind::FormulaScalar
17610                        | crate::engine::vertex::VertexKind::FormulaArray => {
17611                            // plan() already approved allowed overwrites.
17612                            continue;
17613                        }
17614                        _ => {}
17615                    }
17616                }
17617
17618                if let Some(v) = value_probe(graph, cell)
17619                    && !matches!(v, LiteralValue::Empty)
17620                {
17621                    if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17622                        self.region_locks.release(id);
17623                    }
17624                    return Err(ExcelError::new(ExcelErrorKind::Spill)
17625                        .with_message("BlockedByValue")
17626                        .with_extra(ExcelErrorExtra::Spill {
17627                            expected_rows,
17628                            expected_cols,
17629                        }));
17630                }
17631            }
17632        }
17633
17634        let commit_res = graph.commit_spill_region_atomic_with_fault(
17635            anchor_vertex,
17636            targets.to_vec(),
17637            rows,
17638            None,
17639        );
17640        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17641            self.region_locks.release(id);
17642        }
17643        commit_res.map(|_| ())
17644    }
17645
17646    /// Commit a spill and mirror all written cells into Arrow overlay via the owning engine.
17647    pub(crate) fn commit_array_with_overlay<R: EvaluationContext>(
17648        &mut self,
17649        engine: &mut Engine<R>,
17650        anchor_vertex: VertexId,
17651        targets: &[CellRef],
17652        rows: Vec<Vec<LiteralValue>>,
17653    ) -> Result<(), ExcelError> {
17654        if let Err(error) = engine.guard_pending_spill_commit(anchor_vertex, targets) {
17655            self.release_owner(anchor_vertex);
17656            return Err(error);
17657        }
17658        // Re-run plan on concrete targets before committing to respect blockers.
17659        let plan_res = engine.graph.plan_spill_region(anchor_vertex, targets);
17660        if let Err(e) = plan_res {
17661            if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17662                self.region_locks.release(id);
17663            }
17664            return Err(e);
17665        }
17666
17667        let commit_res = engine.graph.commit_spill_region_atomic_with_fault(
17668            anchor_vertex,
17669            targets.to_vec(),
17670            rows.clone(),
17671            None,
17672        );
17673        if let Some(id) = self.active_locks.remove(&anchor_vertex) {
17674            self.region_locks.release(id);
17675        }
17676        commit_res.map(|_| ())?;
17677        engine
17678            .blocked_pending_spills
17679            .retain(|entry| entry.0 != anchor_vertex);
17680        engine.spill_blocker_waiters.forget(anchor_vertex);
17681
17682        // Mirror into Arrow overlay when enabled
17683        if engine.config.arrow_storage_enabled
17684            && engine.config.delta_overlay_enabled
17685            && engine.config.write_formula_overlay_enabled
17686        {
17687            // Expect targets to be a contiguous rectangle row-major starting at some anchor
17688            for (idx, cell) in targets.iter().enumerate() {
17689                let (r_off, c_off) = {
17690                    if rows.is_empty() || rows[0].is_empty() {
17691                        (0usize, 0usize)
17692                    } else {
17693                        let width = rows[0].len();
17694                        (idx / width, idx % width)
17695                    }
17696                };
17697                let v = rows
17698                    .get(r_off)
17699                    .and_then(|r| r.get(c_off))
17700                    .cloned()
17701                    .unwrap_or(LiteralValue::Empty);
17702                let sheet_name = engine.graph.sheet_name(cell.sheet_id).to_string();
17703                engine.mirror_value_to_computed_overlay(
17704                    &sheet_name,
17705                    cell.coord.row() + 1,
17706                    cell.coord.col() + 1,
17707                    &v,
17708                );
17709            }
17710        }
17711        Ok(())
17712    }
17713}
17714
17715impl<R> Engine<R>
17716where
17717    R: EvaluationContext,
17718{
17719    fn resolve_shared_ref(
17720        &self,
17721        reference: &ReferenceType,
17722        current_sheet: &str,
17723    ) -> Result<formualizer_common::SheetRef<'static>, ExcelError> {
17724        use formualizer_common::{
17725            SheetCellRef as SharedCellRef, SheetLocator, SheetRangeRef as SharedRangeRef,
17726            SheetRef as SharedRef,
17727        };
17728
17729        // Preserve anchor flags from the parsed reference when possible.
17730        let sr = match reference {
17731            ReferenceType::Cell {
17732                sheet,
17733                row,
17734                col,
17735                row_abs,
17736                col_abs,
17737            } => {
17738                let row0 = row
17739                    .checked_sub(1)
17740                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17741                let col0 = col
17742                    .checked_sub(1)
17743                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17744                let sheet_loc = match sheet.as_deref() {
17745                    Some(name) => SheetLocator::from_name(name),
17746                    None => SheetLocator::Current,
17747                };
17748                let coord = formualizer_common::RelativeCoord::new(row0, col0, *row_abs, *col_abs);
17749                SharedRef::Cell(SharedCellRef::new(sheet_loc, coord))
17750            }
17751            ReferenceType::Range {
17752                sheet,
17753                start_row,
17754                start_col,
17755                end_row,
17756                end_col,
17757                start_row_abs,
17758                start_col_abs,
17759                end_row_abs,
17760                end_col_abs,
17761            } => {
17762                let sheet_loc = match sheet.as_deref() {
17763                    Some(name) => SheetLocator::from_name(name),
17764                    None => SheetLocator::Current,
17765                };
17766                let sr = start_row
17767                    .map(|r| {
17768                        r.checked_sub(1)
17769                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17770                    })
17771                    .transpose()?;
17772                let sc = start_col
17773                    .map(|c| {
17774                        c.checked_sub(1)
17775                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17776                    })
17777                    .transpose()?;
17778                let er = end_row
17779                    .map(|r| {
17780                        r.checked_sub(1)
17781                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17782                    })
17783                    .transpose()?;
17784                let ec = end_col
17785                    .map(|c| {
17786                        c.checked_sub(1)
17787                            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17788                    })
17789                    .transpose()?;
17790                let range = SharedRangeRef::from_parts(
17791                    sheet_loc,
17792                    sr.map(|idx| formualizer_common::AxisBound::new(idx, *start_row_abs)),
17793                    sc.map(|idx| formualizer_common::AxisBound::new(idx, *start_col_abs)),
17794                    er.map(|idx| formualizer_common::AxisBound::new(idx, *end_row_abs)),
17795                    ec.map(|idx| formualizer_common::AxisBound::new(idx, *end_col_abs)),
17796                )
17797                .map_err(|_| ExcelError::new(ExcelErrorKind::Ref))?;
17798                SharedRef::Range(range)
17799            }
17800            _ => return Err(ExcelError::new(ExcelErrorKind::Ref)),
17801        };
17802
17803        let current_id = self
17804            .graph
17805            .sheet_id(current_sheet)
17806            .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
17807
17808        let resolve_loc = |loc: SheetLocator<'_>| -> Result<SheetLocator<'static>, ExcelError> {
17809            match loc {
17810                SheetLocator::Current => Ok(SheetLocator::Id(current_id)),
17811                SheetLocator::Id(id) => Ok(SheetLocator::Id(id)),
17812                SheetLocator::Name(name) => {
17813                    let n = name.as_ref();
17814                    self.graph
17815                        .sheet_id(n)
17816                        .map(SheetLocator::Id)
17817                        .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))
17818                }
17819            }
17820        };
17821
17822        match sr {
17823            SharedRef::Cell(cell) => {
17824                let owned = cell.into_owned();
17825                let sheet = resolve_loc(owned.sheet)?;
17826                Ok(SharedRef::Cell(SharedCellRef::new(sheet, owned.coord)))
17827            }
17828            SharedRef::Range(range) => {
17829                let owned = range.into_owned();
17830                let sheet = resolve_loc(owned.sheet)?;
17831                Ok(SharedRef::Range(SharedRangeRef {
17832                    sheet,
17833                    start_row: owned.start_row,
17834                    start_col: owned.start_col,
17835                    end_row: owned.end_row,
17836                    end_col: owned.end_col,
17837                }))
17838            }
17839        }
17840    }
17841}
17842
17843// Implement the resolver traits for the Engine.
17844// This allows the interpreter to resolve references by querying the engine's graph.
17845impl<R> crate::traits::ReferenceResolver for Engine<R>
17846where
17847    R: EvaluationContext,
17848{
17849    fn resolve_cell_reference(
17850        &self,
17851        sheet: Option<&str>,
17852        row: u32,
17853        col: u32,
17854    ) -> Result<LiteralValue, ExcelError> {
17855        // This context-free trait method has no knowledge of the formula's
17856        // current sheet, so an unqualified (`None`) reference cannot be resolved
17857        // here. Previously this fell back to `default_sheet_name()`, which leaked
17858        // the reference onto an unrelated sheet (issue #110). Interpreter paths
17859        // already qualify references with the current sheet before reaching this
17860        // method (see `Interpreter::implicit_intersection_from_reference`), and
17861        // the sheet-aware scalar path goes through `resolve_cell_reference_value`
17862        // with an explicit `current_sheet`. Returning #REF! for an unqualified
17863        // reference here surfaces the missing context instead of silently
17864        // returning data from the wrong sheet.
17865        let Some(sheet_name) = sheet else {
17866            return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
17867                "Unqualified cell reference resolved without sheet context".to_string(),
17868            ));
17869        };
17870        // Prefer engine's unified accessor which consults Arrow store for base values
17871        // and falls back to graph for formulas and stored values.
17872        if let Some(v) = self.get_cell_value(sheet_name, row, col) {
17873            Ok(v)
17874        } else {
17875            // Excel semantics: empty cell coerces to 0 in numeric contexts
17876            Ok(LiteralValue::Number(0.0))
17877        }
17878    }
17879}
17880
17881impl<R> crate::traits::RangeResolver for Engine<R>
17882where
17883    R: EvaluationContext,
17884{
17885    fn resolve_range_reference(
17886        &self,
17887        sheet: Option<&str>,
17888        sr: Option<u32>,
17889        sc: Option<u32>,
17890        er: Option<u32>,
17891        ec: Option<u32>,
17892    ) -> Result<Box<dyn crate::traits::Range>, ExcelError> {
17893        // For now, delegate range resolution to the external resolver.
17894        // A future optimization could be to handle this within the graph.
17895        self.resolver.resolve_range_reference(sheet, sr, sc, er, ec)
17896    }
17897}
17898
17899impl<R> crate::traits::NamedRangeResolver for Engine<R>
17900where
17901    R: EvaluationContext,
17902{
17903    fn resolve_named_range_reference(
17904        &self,
17905        name: &str,
17906    ) -> Result<Vec<Vec<LiteralValue>>, ExcelError> {
17907        self.resolver.resolve_named_range_reference(name)
17908    }
17909}
17910
17911impl<R> crate::traits::TableResolver for Engine<R>
17912where
17913    R: EvaluationContext,
17914{
17915    fn resolve_table_reference(
17916        &self,
17917        tref: &formualizer_parse::parser::TableReference,
17918    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
17919        self.resolver.resolve_table_reference(tref)
17920    }
17921}
17922
17923impl<R> crate::traits::SourceResolver for Engine<R>
17924where
17925    R: EvaluationContext,
17926{
17927    fn source_scalar_version(&self, name: &str) -> Option<u64> {
17928        self.resolver.source_scalar_version(name)
17929    }
17930
17931    fn resolve_source_scalar(&self, name: &str) -> Result<LiteralValue, ExcelError> {
17932        self.resolver.resolve_source_scalar(name)
17933    }
17934
17935    fn source_table_version(&self, name: &str) -> Option<u64> {
17936        self.resolver.source_table_version(name)
17937    }
17938
17939    fn resolve_source_table(
17940        &self,
17941        name: &str,
17942    ) -> Result<Box<dyn crate::traits::Table>, ExcelError> {
17943        self.resolver.resolve_source_table(name)
17944    }
17945}
17946
17947// The Engine is a Resolver because it implements the constituent traits.
17948impl<R> crate::traits::Resolver for Engine<R> where R: EvaluationContext {}
17949
17950// The Engine provides functions by delegating to its internal resolver.
17951impl<R> crate::traits::FunctionProvider for Engine<R>
17952where
17953    R: EvaluationContext,
17954{
17955    fn planning_semantic_revision(&self) -> Option<u64> {
17956        self.resolver.planning_semantic_revision()
17957    }
17958
17959    fn get_function(
17960        &self,
17961        prefix: &str,
17962        name: &str,
17963    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
17964        self.resolver.get_function(prefix, name)
17965    }
17966
17967    fn get_function_for_planning(
17968        &self,
17969        prefix: &str,
17970        name: &str,
17971    ) -> Option<std::sync::Arc<dyn crate::function::Function>> {
17972        self.resolver.get_function_for_planning(prefix, name)
17973    }
17974}
17975
17976impl<R> Engine<R>
17977where
17978    R: EvaluationContext,
17979{
17980    /// Semantic used coordinates exclude graph-only dependency placeholders.
17981    ///
17982    /// Non-empty base/overlay/computed cells come from Arrow storage, while
17983    /// scalar and array formulas come from graph formula kinds even before
17984    /// their results are materialized. The legacy graph fallback is omitted:
17985    /// `load_packed_to_vertex` entries are either represented by those sources
17986    /// or are `Empty` dependency placeholders, not a third value authority.
17987    pub(crate) fn semantic_used_rows_for_columns(
17988        &self,
17989        sheet: &str,
17990        start_col: u32,
17991        end_col: u32,
17992    ) -> Option<(u32, u32)> {
17993        let arrow_bounds = self
17994            .sheet_store()
17995            .sheet(sheet)
17996            .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
17997        let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
17998        Self::union_used_bounds(arrow_bounds, formula_bounds)
17999    }
18000
18001    pub(crate) fn semantic_used_cols_for_rows(
18002        &self,
18003        sheet: &str,
18004        start_row: u32,
18005        end_row: u32,
18006    ) -> Option<(u32, u32)> {
18007        let arrow_bounds = self
18008            .sheet_store()
18009            .sheet(sheet)
18010            .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
18011        let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
18012        Self::union_used_bounds(arrow_bounds, formula_bounds)
18013    }
18014}
18015
18016// Override EvaluationContext to provide thread pool access
18017impl<R> crate::traits::EvaluationContext for Engine<R>
18018where
18019    R: EvaluationContext,
18020{
18021    fn clock(&self) -> &dyn crate::timezone::ClockProvider {
18022        &self.clock
18023    }
18024
18025    fn thread_pool(&self) -> Option<&Arc<rayon::ThreadPool>> {
18026        self.thread_pool.as_ref()
18027    }
18028
18029    fn cancellation_token(&self) -> Option<crate::engine::CancelToken> {
18030        self.active_cancel_flag.clone()
18031    }
18032
18033    fn chunk_hint(&self) -> Option<usize> {
18034        // Use a simple heuristic from configuration (stripe width * height) as a default hint.
18035        let hint =
18036            (self.config.stripe_height as usize).saturating_mul(self.config.stripe_width as usize);
18037        Some(hint.clamp(1024, 1 << 20)) // clamp between 1K and ~1M
18038    }
18039
18040    fn volatile_level(&self) -> crate::traits::VolatileLevel {
18041        self.config.volatile_level
18042    }
18043
18044    fn workbook_seed(&self) -> u64 {
18045        self.config.workbook_seed
18046    }
18047
18048    fn recalc_epoch(&self) -> u64 {
18049        self.recalc_epoch
18050    }
18051
18052    fn workbook_sheet_count(&self) -> Option<usize> {
18053        Some(self.graph.sheet_reg().active_len())
18054    }
18055
18056    fn sheet_index_by_name(&self, sheet: &str) -> Option<usize> {
18057        self.graph.sheet_reg().active_position(sheet)
18058    }
18059
18060    fn current_sheet_index(&self, current_sheet: &str) -> Option<usize> {
18061        self.sheet_index_by_name(current_sheet)
18062    }
18063
18064    fn inspect_reference(
18065        &self,
18066        reference: &ReferenceType,
18067        current_sheet: &str,
18068    ) -> Result<Option<ReferenceInfo>, ExcelError> {
18069        let sheet_info = |sheet_name: &str| -> Result<(SheetId, usize), ExcelError> {
18070            let sheet_id = self
18071                .graph
18072                .sheet_id(sheet_name)
18073                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
18074            let sheet_index = self
18075                .graph
18076                .sheet_reg()
18077                .active_position_by_id(sheet_id)
18078                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
18079            Ok((sheet_id, sheet_index))
18080        };
18081
18082        let cell_info =
18083            |sheet_name: &str, row: u32, col: u32| -> Result<ReferenceInfo, ExcelError> {
18084                let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
18085                let row0 = row
18086                    .checked_sub(1)
18087                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
18088                let col0 = col
18089                    .checked_sub(1)
18090                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
18091                Ok(ReferenceInfo {
18092                    first_sheet_index: Some(sheet_index),
18093                    sheet_count: Some(1),
18094                    first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
18095                })
18096            };
18097
18098        let range_info = |sheet_name: &str,
18099                          start_row: Option<u32>,
18100                          start_col: Option<u32>|
18101         -> Result<ReferenceInfo, ExcelError> {
18102            let (sheet_id, sheet_index) = sheet_info(sheet_name)?;
18103            let row = start_row.unwrap_or(1);
18104            let col = start_col.unwrap_or(1);
18105            let row0 = row
18106                .checked_sub(1)
18107                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
18108            let col0 = col
18109                .checked_sub(1)
18110                .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
18111            Ok(ReferenceInfo {
18112                first_sheet_index: Some(sheet_index),
18113                sheet_count: Some(1),
18114                first_cell: Some(CellRef::new(sheet_id, Coord::new(row0, col0, true, true))),
18115            })
18116        };
18117
18118        let info = match reference {
18119            ReferenceType::Cell {
18120                sheet, row, col, ..
18121            } => {
18122                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
18123                cell_info(sheet_name, *row, *col)?
18124            }
18125            ReferenceType::Range {
18126                sheet,
18127                start_row,
18128                start_col,
18129                ..
18130            } => {
18131                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
18132                range_info(sheet_name, *start_row, *start_col)?
18133            }
18134            ReferenceType::Cell3D {
18135                sheet_first,
18136                sheet_last,
18137                row,
18138                col,
18139                ..
18140            } => {
18141                let first = cell_info(sheet_first, *row, *col)?;
18142                ReferenceInfo {
18143                    first_sheet_index: first.first_sheet_index,
18144                    sheet_count: self
18145                        .graph
18146                        .sheet_reg()
18147                        .active_span_len(sheet_first, sheet_last),
18148                    first_cell: first.first_cell,
18149                }
18150            }
18151            ReferenceType::Range3D {
18152                sheet_first,
18153                sheet_last,
18154                start_row,
18155                start_col,
18156                ..
18157            } => {
18158                let first = range_info(sheet_first, *start_row, *start_col)?;
18159                ReferenceInfo {
18160                    first_sheet_index: first.first_sheet_index,
18161                    sheet_count: self
18162                        .graph
18163                        .sheet_reg()
18164                        .active_span_len(sheet_first, sheet_last),
18165                    first_cell: first.first_cell,
18166                }
18167            }
18168            ReferenceType::NamedRange(name) => {
18169                let current_id = self
18170                    .graph
18171                    .sheet_id(current_sheet)
18172                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
18173                let named = self
18174                    .graph
18175                    .resolve_name_entry(name, current_id)
18176                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
18177                match &named.definition {
18178                    NamedDefinition::Cell(cell) => ReferenceInfo {
18179                        first_sheet_index: self
18180                            .graph
18181                            .sheet_reg()
18182                            .active_position_by_id(cell.sheet_id),
18183                        sheet_count: Some(1),
18184                        first_cell: Some(*cell),
18185                    },
18186                    NamedDefinition::Range(range) => ReferenceInfo {
18187                        first_sheet_index: self
18188                            .graph
18189                            .sheet_reg()
18190                            .active_position_by_id(range.start.sheet_id),
18191                        sheet_count: Some(1),
18192                        first_cell: Some(range.start),
18193                    },
18194                    NamedDefinition::Literal(_) | NamedDefinition::Formula { .. } => {
18195                        ReferenceInfo {
18196                            first_sheet_index: None,
18197                            sheet_count: None,
18198                            first_cell: None,
18199                        }
18200                    }
18201                }
18202            }
18203            ReferenceType::Table(tref) => {
18204                let table = self
18205                    .graph
18206                    .resolve_table_entry(&tref.name)
18207                    .ok_or_else(|| ExcelError::new(ExcelErrorKind::Ref))?;
18208                ReferenceInfo {
18209                    first_sheet_index: self
18210                        .graph
18211                        .sheet_reg()
18212                        .active_position_by_id(table.range.start.sheet_id),
18213                    sheet_count: Some(1),
18214                    first_cell: Some(table.range.start),
18215                }
18216            }
18217            ReferenceType::External(_) => return Err(ExcelError::new(ExcelErrorKind::Ref)),
18218        };
18219
18220        Ok(Some(info))
18221    }
18222
18223    fn resolve_spill_reference(
18224        &self,
18225        anchor: &ReferenceType,
18226        current_sheet: &str,
18227    ) -> Result<ReferenceType, ExcelError> {
18228        let no_spill = || {
18229            ExcelError::new(ExcelErrorKind::Ref)
18230                .with_message("Spill reference anchor has no current spill")
18231        };
18232        // The rectangle keeps the operand's sheet qualification: an
18233        // unqualified anchor on the current sheet yields an unqualified range.
18234        let mut qualify = true;
18235        let anchor_cell = match anchor {
18236            ReferenceType::Cell {
18237                sheet, row, col, ..
18238            } => {
18239                qualify = sheet.is_some();
18240                let sheet_name = sheet.as_deref().unwrap_or(current_sheet);
18241                let sheet_id = self.graph.sheet_id(sheet_name).ok_or_else(no_spill)?;
18242                if *row == 0 || *col == 0 {
18243                    return Err(no_spill());
18244                }
18245                CellRef::new(sheet_id, Coord::from_excel(*row, *col, true, true))
18246            }
18247            ReferenceType::NamedRange(name) => {
18248                let current_id = self.graph.sheet_id(current_sheet).ok_or_else(no_spill)?;
18249                let named = self
18250                    .graph
18251                    .resolve_name_entry(name, current_id)
18252                    .ok_or_else(no_spill)?;
18253                match &named.definition {
18254                    NamedDefinition::Cell(cell) => *cell,
18255                    NamedDefinition::Range(range)
18256                        if range.start.sheet_id == range.end.sheet_id
18257                            && range.start.coord.row() == range.end.coord.row()
18258                            && range.start.coord.col() == range.end.coord.col() =>
18259                    {
18260                        range.start
18261                    }
18262                    _ => {
18263                        return Err(ExcelError::new(ExcelErrorKind::Ref)
18264                            .with_message("Spill reference name must refer to a single cell"));
18265                    }
18266                }
18267            }
18268            _ => {
18269                return Err(ExcelError::new(ExcelErrorKind::Ref)
18270                    .with_message("Spill reference operand must be a single cell"));
18271            }
18272        };
18273        let vertex = self
18274            .graph
18275            .get_vertex_id_for_address(&anchor_cell)
18276            .ok_or_else(no_spill)?;
18277        // Policy: the spill operator applies only to dynamic arrays, never
18278        // to a legacy fixed-extent formula (including a single-cell one).
18279        if self.graph.fixed_single_arrays.contains(&vertex)
18280            || self.graph.fixed_array_shapes.contains_key(&vertex)
18281        {
18282            return Err(ExcelError::new(ExcelErrorKind::Ref)
18283                .with_message("Spill reference to a fixed array formula"));
18284        }
18285        let (first, last) = match self.graph.spill_extent_for_anchor(vertex) {
18286            Some(extent) => extent,
18287            // FORM211: a declared anchor (source spill identity) holding a
18288            // current scalar result is a 1x1 spill. A malformed registry
18289            // entry stays `#REF!`, as do blocked/error/empty results.
18290            None if !self.graph.spill_registry_has_anchor(vertex)
18291                && self.graph.is_current_declared_dynamic_anchor(vertex)
18292                && self.declared_anchor_holds_scalar(anchor_cell) =>
18293            {
18294                (anchor_cell, anchor_cell)
18295            }
18296            None => return Err(no_spill()),
18297        };
18298        let sheet_name = qualify.then(|| self.graph.sheet_name(first.sheet_id).to_string());
18299        let (sr, sc) = (first.coord.row() + 1, first.coord.col() + 1);
18300        let (er, ec) = (last.coord.row() + 1, last.coord.col() + 1);
18301        // The rectangle is concrete, so its bounds are absolute.
18302        Ok(if sr == er && sc == ec {
18303            ReferenceType::cell_with_abs(sheet_name, sr, sc, true, true)
18304        } else {
18305            ReferenceType::Range {
18306                sheet: sheet_name,
18307                start_row: Some(sr),
18308                start_col: Some(sc),
18309                end_row: Some(er),
18310                end_col: Some(ec),
18311                start_row_abs: true,
18312                start_col_abs: true,
18313                end_row_abs: true,
18314                end_col_abs: true,
18315            }
18316        })
18317    }
18318
18319    fn formula_text_at_cell(&self, cell: CellRef) -> Result<Option<String>, ExcelError> {
18320        let sheet_name = self.graph.sheet_name(cell.sheet_id);
18321        if sheet_name.is_empty() {
18322            return Err(ExcelError::new(ExcelErrorKind::Ref));
18323        }
18324        let row = cell.coord.row() + 1;
18325        let col = cell.coord.col() + 1;
18326
18327        if let Some(entries) = self.staged_formulas.get(sheet_name)
18328            && let Some(text) = entries.get(row, col)
18329        {
18330            return Ok(Some(if text.starts_with('=') {
18331                text.to_owned()
18332            } else {
18333                format!("={text}")
18334            }));
18335        }
18336
18337        let Some((Some(ast), _)) = self.get_cell(sheet_name, row, col) else {
18338            return Ok(None);
18339        };
18340        Ok(Some(formualizer_parse::pretty::canonical_formula(&ast)))
18341    }
18342
18343    fn used_rows_for_columns(
18344        &self,
18345        sheet: &str,
18346        start_col: u32,
18347        end_col: u32,
18348    ) -> Option<(u32, u32)> {
18349        // Union Arrow-backed used-region with formula rows that have not been materialized yet.
18350        let sheet_id = self.graph.sheet_id(sheet)?;
18351        let snap = self.data_snapshot_id();
18352        if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
18353            guard
18354                .as_ref()
18355                .and_then(|cache| cache.get_row_bounds(sheet_id, start_col, end_col, snap))
18356        }) {
18357            return cached;
18358        }
18359
18360        let arrow_bounds = self
18361            .sheet_store()
18362            .sheet(sheet)
18363            .and_then(|_| self.arrow_used_row_bounds(sheet, start_col, end_col));
18364        let formula_bounds = self.formula_row_bounds_for_columns(sheet, start_col, end_col);
18365        let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
18366            Some(bounds)
18367        } else {
18368            let sc0 = start_col.saturating_sub(1);
18369            let ec0 = end_col.saturating_sub(1);
18370            self.graph
18371                .used_row_bounds_for_columns(sheet_id, sc0, ec0)
18372                .map(|(a0, b0)| (a0 + 1, b0 + 1))
18373        };
18374
18375        if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
18376            guard
18377                .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
18378                .put_row_bounds(sheet_id, start_col, end_col, snap, computed);
18379        }
18380
18381        computed
18382    }
18383
18384    fn used_cols_for_rows(&self, sheet: &str, start_row: u32, end_row: u32) -> Option<(u32, u32)> {
18385        // Union Arrow-backed used-region with formula columns that have not been materialized yet.
18386        let sheet_id = self.graph.sheet_id(sheet)?;
18387        let snap = self.data_snapshot_id();
18388        if let Some(cached) = self.used_axis_bounds_cache.read().ok().and_then(|guard| {
18389            guard
18390                .as_ref()
18391                .and_then(|cache| cache.get_col_bounds(sheet_id, start_row, end_row, snap))
18392        }) {
18393            return cached;
18394        }
18395
18396        let arrow_bounds = self
18397            .sheet_store()
18398            .sheet(sheet)
18399            .and_then(|_| self.arrow_used_col_bounds(sheet, start_row, end_row));
18400        let formula_bounds = self.formula_col_bounds_for_rows(sheet, start_row, end_row);
18401        let computed = if let Some(bounds) = Self::union_used_bounds(arrow_bounds, formula_bounds) {
18402            Some(bounds)
18403        } else {
18404            let sr0 = start_row.saturating_sub(1);
18405            let er0 = end_row.saturating_sub(1);
18406            self.graph
18407                .used_col_bounds_for_rows(sheet_id, sr0, er0)
18408                .map(|(a0, b0)| (a0 + 1, b0 + 1))
18409        };
18410
18411        if let Ok(mut guard) = self.used_axis_bounds_cache.write() {
18412            guard
18413                .get_or_insert_with(|| UsedAxisBoundsCache::new(snap))
18414                .put_col_bounds(sheet_id, start_row, end_row, snap, computed);
18415        }
18416
18417        computed
18418    }
18419
18420    fn sheet_bounds(&self, sheet: &str) -> Option<(u32, u32)> {
18421        let _ = self.graph.sheet_id(sheet)?;
18422        // Excel-like upper bounds; we expose something finite but large.
18423        // Backends may override with real bounds.
18424        Some((1_048_576, 16_384)) // 1048576 rows, 16384 cols (XFD)
18425    }
18426
18427    fn data_snapshot_id(&self) -> u64 {
18428        self.snapshot_id.load(std::sync::atomic::Ordering::Relaxed)
18429    }
18430
18431    fn backend_caps(&self) -> crate::traits::BackendCaps {
18432        crate::traits::BackendCaps {
18433            streaming: true,
18434            used_region: true,
18435            write: false,
18436            tables: false,
18437            async_stream: false,
18438        }
18439    }
18440
18441    fn build_lookup_index(
18442        &self,
18443        view: &RangeView<'_>,
18444        axis: LookupAxis,
18445    ) -> Option<Arc<LookupIndex>> {
18446        self.build_lookup_index_impl(view, axis)
18447    }
18448
18449    // Flats removed
18450
18451    fn date_system(&self) -> crate::engine::DateSystem {
18452        self.config.date_system
18453    }
18454    /// New: resolve a reference into a RangeView (Phase 2 API)
18455    fn resolve_range_view<'c>(
18456        &'c self,
18457        reference: &ReferenceType,
18458        current_sheet: &str,
18459    ) -> Result<RangeView<'c>, ExcelError> {
18460        match reference {
18461            ReferenceType::External(ext) => {
18462                let name = ext.raw.as_str();
18463                match ext.kind {
18464                    formualizer_parse::parser::ExternalRefKind::Cell { .. } => {
18465                        let Some(source) = self.graph.resolve_source_scalar_entry(name) else {
18466                            return Err(ExcelError::new(ExcelErrorKind::Name)
18467                                .with_message(format!("Undefined name: {name}")));
18468                        };
18469                        let version = source
18470                            .version
18471                            .or_else(|| self.resolver.source_scalar_version(name));
18472                        let v = self.resolve_source_scalar_cached(name, version)?;
18473                        Ok(RangeView::from_owned_rows(
18474                            vec![vec![v]],
18475                            self.config.date_system,
18476                        ))
18477                    }
18478                    formualizer_parse::parser::ExternalRefKind::Range { .. } => {
18479                        let Some(source) = self.graph.resolve_source_table_entry(name) else {
18480                            // A deferred (whole-row/column) external range
18481                            // evaluates to #REF!, as it did before the parser
18482                            // kept it whole (see `unbound_external_range_defers`).
18483                            let kind =
18484                                if crate::engine::refs::unbound_external_range_defers(&ext.kind) {
18485                                    ExcelErrorKind::Ref
18486                                } else {
18487                                    ExcelErrorKind::Name
18488                                };
18489                            return Err(ExcelError::new(kind)
18490                                .with_message(format!("Undefined table: {name}")));
18491                        };
18492                        let version = source
18493                            .version
18494                            .or_else(|| self.resolver.source_table_version(name));
18495                        let table = self.resolve_source_table_cached(name, version)?;
18496                        let spec = Some(formualizer_parse::parser::TableSpecifier::Data);
18497                        self.source_table_to_range_view(table.as_ref(), &spec)
18498                    }
18499                }
18500            }
18501            ReferenceType::Range { .. } => {
18502                let shared = self.resolve_shared_ref(reference, current_sheet)?;
18503                let formualizer_common::SheetRef::Range(range) = shared else {
18504                    return Err(ExcelError::new(ExcelErrorKind::Ref));
18505                };
18506                // No context sheet is available here, so an unresolved locator
18507                // is #REF! rather than a guess (issue #110).
18508                let sheet_id = match range.sheet {
18509                    formualizer_common::SheetLocator::Id(id) => id,
18510                    formualizer_common::SheetLocator::Current
18511                    | formualizer_common::SheetLocator::Name(_) => {
18512                        return Err(ExcelError::new(ExcelErrorKind::Ref));
18513                    }
18514                };
18515                let sheet_name = self.graph.sheet_name(sheet_id);
18516
18517                let bounded_range = if range.start_row.is_some()
18518                    && range.start_col.is_some()
18519                    && range.end_row.is_some()
18520                    && range.end_col.is_some()
18521                {
18522                    Some(RangeRef::try_from_shared(range.as_ref())?)
18523                } else {
18524                    None
18525                };
18526
18527                let sr = bounded_range
18528                    .as_ref()
18529                    .map(|r| r.start.coord.row() + 1)
18530                    .or_else(|| range.start_row.map(|b| b.index + 1));
18531                let sc = bounded_range
18532                    .as_ref()
18533                    .map(|r| r.start.coord.col() + 1)
18534                    .or_else(|| range.start_col.map(|b| b.index + 1));
18535                let er = bounded_range
18536                    .as_ref()
18537                    .map(|r| r.end.coord.row() + 1)
18538                    .or_else(|| range.end_row.map(|b| b.index + 1));
18539                let ec = bounded_range
18540                    .as_ref()
18541                    .map(|r| r.end.coord.col() + 1)
18542                    .or_else(|| range.end_col.map(|b| b.index + 1));
18543
18544                let extent = resolve_used_extent_with_fallback(
18545                    OpenRangeBounds {
18546                        start_row: sr,
18547                        start_column: sc,
18548                        end_row: er,
18549                        end_column: ec,
18550                    },
18551                    ExtentPolicy::EvaluationCompat {
18552                        fallback_row: None,
18553                        fallback_column: None,
18554                    },
18555                    || {
18556                        self.sheet_bounds(sheet_name)
18557                            .map(|_| self.config.max_open_ended_rows)
18558                    },
18559                    || {
18560                        self.sheet_bounds(sheet_name)
18561                            .map(|_| self.config.max_open_ended_cols)
18562                    },
18563                    |first, last| self.used_rows_for_columns(sheet_name, first, last),
18564                    |first, last| self.used_cols_for_rows(sheet_name, first, last),
18565                );
18566                let (sr, sc, er, ec) = extent
18567                    .map(|extent| {
18568                        (
18569                            extent.start_row,
18570                            extent.start_column,
18571                            extent.end_row,
18572                            extent.end_column,
18573                        )
18574                    })
18575                    .unwrap_or((1, 1, 0, 0));
18576
18577                if self.force_materialize_range_views {
18578                    if er < sr || ec < sc {
18579                        return Ok(RangeView::from_owned_rows(
18580                            Vec::new(),
18581                            self.config.date_system,
18582                        ));
18583                    }
18584                    let h = (er - sr + 1) as u64;
18585                    let w = (ec - sc + 1) as u64;
18586                    let cell_count = h.saturating_mul(w);
18587                    if cell_count <= self.config.spill.max_spill_cells as u64 {
18588                        let mut rows: Vec<Vec<LiteralValue>> = Vec::with_capacity(h as usize);
18589                        for r in sr..=er {
18590                            let mut rowv: Vec<LiteralValue> = Vec::with_capacity(w as usize);
18591                            for c in sc..=ec {
18592                                rowv.push(
18593                                    self.get_cell_value(sheet_name, r, c)
18594                                        .unwrap_or(LiteralValue::Empty),
18595                                );
18596                            }
18597                            rows.push(rowv);
18598                        }
18599                        return Ok(RangeView::from_owned_rows(rows, self.config.date_system));
18600                    }
18601                }
18602
18603                let Some(asheet) = self.sheet_store().sheet(sheet_name) else {
18604                    return Ok(RangeView::from_owned_rows(
18605                        Vec::new(),
18606                        self.config.date_system,
18607                    ));
18608                };
18609
18610                let rv = if er < sr || ec < sc {
18611                    asheet.range_view(1, 1, 0, 0)
18612                } else {
18613                    let sr0 = sr.saturating_sub(1) as usize;
18614                    let sc0 = sc.saturating_sub(1) as usize;
18615                    let er0 = er.saturating_sub(1) as usize;
18616                    let ec0 = ec.saturating_sub(1) as usize;
18617                    asheet.range_view(sr0, sc0, er0, ec0)
18618                };
18619
18620                Ok(rv)
18621            }
18622            ReferenceType::Cell { .. } => {
18623                let shared = self.resolve_shared_ref(reference, current_sheet)?;
18624                let formualizer_common::SheetRef::Cell(cell) = shared else {
18625                    return Err(ExcelError::new(ExcelErrorKind::Ref));
18626                };
18627                let addr = CellRef::try_from_shared(cell)?;
18628                let sheet_id = addr.sheet_id;
18629                let sheet_name = self.graph.sheet_name(sheet_id);
18630                let row = addr.coord.row() + 1;
18631                let col = addr.coord.col() + 1;
18632
18633                if self.force_materialize_range_views {
18634                    let v = self
18635                        .get_cell_value(sheet_name, row, col)
18636                        .unwrap_or(LiteralValue::Empty);
18637                    return Ok(RangeView::from_owned_rows(
18638                        vec![vec![v]],
18639                        self.config.date_system,
18640                    ));
18641                }
18642
18643                if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
18644                    let r0 = row.saturating_sub(1) as usize;
18645                    let c0 = col.saturating_sub(1) as usize;
18646                    let rv = asheet.range_view(r0, c0, r0, c0);
18647                    Ok(rv)
18648                } else {
18649                    let v = self
18650                        .get_cell_value(sheet_name, row, col)
18651                        .unwrap_or(LiteralValue::Empty);
18652                    Ok(RangeView::from_owned_rows(
18653                        vec![vec![v]],
18654                        self.config.date_system,
18655                    ))
18656                }
18657            }
18658            ReferenceType::NamedRange(name) => {
18659                if let Some(current_id) = self.graph.sheet_id(current_sheet)
18660                    && let Some(named) = self.graph.resolve_name_entry(name, current_id)
18661                {
18662                    match &named.definition {
18663                        NamedDefinition::Cell(cell_ref) => {
18664                            let sheet_name = self.graph.sheet_name(cell_ref.sheet_id);
18665                            if self.force_materialize_range_views {
18666                                let v = self
18667                                    .get_cell_value(
18668                                        sheet_name,
18669                                        cell_ref.coord.row() + 1,
18670                                        cell_ref.coord.col() + 1,
18671                                    )
18672                                    .unwrap_or(LiteralValue::Empty);
18673                                return Ok(RangeView::from_owned_rows(
18674                                    vec![vec![v]],
18675                                    self.config.date_system,
18676                                ));
18677                            } else {
18678                                let asheet = self
18679                                    .sheet_store()
18680                                    .sheet(sheet_name)
18681                                    .expect("Arrow sheet missing for named cell");
18682                                let r0 = cell_ref.coord.row() as usize;
18683                                let c0 = cell_ref.coord.col() as usize;
18684                                let rv = asheet.range_view(r0, c0, r0, c0);
18685                                return Ok(rv);
18686                            }
18687                        }
18688                        NamedDefinition::Range(range_ref) => {
18689                            let sheet_name = self.graph.sheet_name(range_ref.start.sheet_id);
18690                            let sr = range_ref.start.coord.row() + 1;
18691                            let sc = range_ref.start.coord.col() + 1;
18692                            let er = range_ref.end.coord.row() + 1;
18693                            let ec = range_ref.end.coord.col() + 1;
18694                            if self.force_materialize_range_views {
18695                                let h = (er.saturating_sub(sr) + 1) as u64;
18696                                let w = (ec.saturating_sub(sc) + 1) as u64;
18697                                let cell_count = h.saturating_mul(w);
18698                                if cell_count <= self.config.spill.max_spill_cells as u64 {
18699                                    let mut rows: Vec<Vec<LiteralValue>> =
18700                                        Vec::with_capacity(h as usize);
18701                                    for r in sr..=er {
18702                                        let mut rowv: Vec<LiteralValue> =
18703                                            Vec::with_capacity(w as usize);
18704                                        for c in sc..=ec {
18705                                            rowv.push(
18706                                                self.get_cell_value(sheet_name, r, c)
18707                                                    .unwrap_or(LiteralValue::Empty),
18708                                            );
18709                                        }
18710                                        rows.push(rowv);
18711                                    }
18712                                    return Ok(RangeView::from_owned_rows(
18713                                        rows,
18714                                        self.config.date_system,
18715                                    ));
18716                                }
18717                            }
18718                            let asheet = self
18719                                .sheet_store()
18720                                .sheet(sheet_name)
18721                                .expect("Arrow sheet missing for named range");
18722                            let sr0 = range_ref.start.coord.row() as usize;
18723                            let sc0 = range_ref.start.coord.col() as usize;
18724                            let er0 = range_ref.end.coord.row() as usize;
18725                            let ec0 = range_ref.end.coord.col() as usize;
18726                            let rv = asheet.range_view(sr0, sc0, er0, ec0);
18727                            return Ok(rv);
18728                        }
18729                        NamedDefinition::Literal(v) => {
18730                            return Ok(RangeView::from_owned_rows(
18731                                vec![vec![v.clone()]],
18732                                self.config.date_system,
18733                            ));
18734                        }
18735                        NamedDefinition::Formula { .. } => {
18736                            if let Some(value) = self.graph.get_value(named.vertex) {
18737                                return Ok(RangeView::from_owned_rows(
18738                                    vec![vec![value]],
18739                                    self.config.date_system,
18740                                ));
18741                            }
18742                        }
18743                    }
18744                }
18745
18746                if let Some(source) = self.graph.resolve_source_scalar_entry(name) {
18747                    let version = source
18748                        .version
18749                        .or_else(|| self.resolver.source_scalar_version(name));
18750                    let v = self.resolve_source_scalar_cached(name, version)?;
18751                    return Ok(RangeView::from_owned_rows(
18752                        vec![vec![v]],
18753                        self.config.date_system,
18754                    ));
18755                }
18756
18757                let data = self.resolver.resolve_named_range_reference(name)?;
18758                Ok(RangeView::from_owned_rows(data, self.config.date_system))
18759            }
18760            ReferenceType::Table(tref) => {
18761                if let Some(table) = self.graph.resolve_table_entry(&tref.name) {
18762                    let sheet_name = self.graph.sheet_name(table.range.start.sheet_id);
18763                    let asheet = self
18764                        .sheet_store()
18765                        .sheet(sheet_name)
18766                        .expect("Arrow sheet missing for table reference");
18767
18768                    let sr0 = table.range.start.coord.row() as usize;
18769                    let sc0 = table.range.start.coord.col() as usize;
18770                    let er0 = table.range.end.coord.row() as usize;
18771                    let ec0 = table.range.end.coord.col() as usize;
18772
18773                    let has_totals = table.totals_row;
18774                    let has_headers = table.header_row;
18775                    let data_sr = if has_headers {
18776                        sr0.saturating_add(1)
18777                    } else {
18778                        sr0
18779                    };
18780                    let data_er = if has_totals {
18781                        er0.saturating_sub(1)
18782                    } else {
18783                        er0
18784                    };
18785
18786                    let select = |sr: usize, sc: usize, er: usize, ec: usize| {
18787                        if sr > er || sc > ec {
18788                            asheet.range_view(1, 1, 0, 0)
18789                        } else {
18790                            asheet.range_view(sr, sc, er, ec)
18791                        }
18792                    };
18793
18794                    let av = match &tref.specifier {
18795                        None => {
18796                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18797                                "Table reference without specifier is unsupported".to_string(),
18798                            ));
18799                        }
18800                        Some(formualizer_parse::parser::TableSpecifier::Column(col)) => {
18801                            let Some(idx) = table.col_index(col) else {
18802                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
18803                                    "Column refers to unknown table column".to_string(),
18804                                ));
18805                            };
18806                            let c0 = sc0 + idx;
18807                            select(data_sr, c0, data_er, c0)
18808                        }
18809                        Some(formualizer_parse::parser::TableSpecifier::ColumnRange(
18810                            start,
18811                            end,
18812                        )) => {
18813                            let Some(si) = table.col_index(start) else {
18814                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
18815                                    "Column range refers to unknown column(s)".to_string(),
18816                                ));
18817                            };
18818                            let Some(ei) = table.col_index(end) else {
18819                                return Err(ExcelError::new(ExcelErrorKind::Ref).with_message(
18820                                    "Column range refers to unknown column(s)".to_string(),
18821                                ));
18822                            };
18823                            let (mut a, mut b) = (si, ei);
18824                            if a > b {
18825                                std::mem::swap(&mut a, &mut b);
18826                            }
18827                            let c_start = sc0 + a;
18828                            let c_end = sc0 + b;
18829                            select(data_sr, c_start, data_er, c_end)
18830                        }
18831                        Some(formualizer_parse::parser::TableSpecifier::All)
18832                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18833                            formualizer_parse::parser::SpecialItem::All,
18834                        )) => select(sr0, sc0, er0, ec0),
18835                        Some(formualizer_parse::parser::TableSpecifier::Data)
18836                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18837                            formualizer_parse::parser::SpecialItem::Data,
18838                        )) => select(data_sr, sc0, data_er, ec0),
18839                        Some(formualizer_parse::parser::TableSpecifier::Headers)
18840                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18841                            formualizer_parse::parser::SpecialItem::Headers,
18842                        )) => {
18843                            if !has_headers {
18844                                asheet.range_view(1, 1, 0, 0)
18845                            } else {
18846                                select(sr0, sc0, sr0, ec0)
18847                            }
18848                        }
18849                        Some(formualizer_parse::parser::TableSpecifier::Totals)
18850                        | Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18851                            formualizer_parse::parser::SpecialItem::Totals,
18852                        )) => {
18853                            if !has_totals {
18854                                asheet.range_view(1, 1, 0, 0)
18855                            } else {
18856                                select(er0, sc0, er0, ec0)
18857                            }
18858                        }
18859                        Some(formualizer_parse::parser::TableSpecifier::SpecialItem(
18860                            formualizer_parse::parser::SpecialItem::ThisRow,
18861                        )) => {
18862                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18863                                "@ (This Row) requires table-aware context; not yet supported"
18864                                    .to_string(),
18865                            ));
18866                        }
18867                        Some(formualizer_parse::parser::TableSpecifier::Row(_))
18868                        | Some(formualizer_parse::parser::TableSpecifier::Combination(_)) => {
18869                            return Err(ExcelError::new(ExcelErrorKind::NImpl).with_message(
18870                                "Complex structured references not yet supported".to_string(),
18871                            ));
18872                        }
18873                    };
18874
18875                    return Ok(av);
18876                }
18877
18878                if let Some(source) = self.graph.resolve_source_table_entry(&tref.name) {
18879                    let version = source
18880                        .version
18881                        .or_else(|| self.resolver.source_table_version(&tref.name));
18882                    let table = self.resolve_source_table_cached(&tref.name, version)?;
18883                    return self.source_table_to_range_view(table.as_ref(), &tref.specifier);
18884                }
18885
18886                // Fallback: materialize via Resolver::resolve_range_like tranche 1.
18887                // A table nobody defines (an unbound reference kept by the
18888                // BestEffort preparation policy) is `#NAME?`, the kind Strict
18889                // reports at preparation, not the resolver's "not implemented".
18890                let boxed = self
18891                    .resolve_range_like(&ReferenceType::Table(tref.clone()))
18892                    .map_err(|e| {
18893                        if e.kind == ExcelErrorKind::NImpl {
18894                            ExcelError::new(ExcelErrorKind::Name)
18895                                .with_message(format!("Unknown table: {}", tref.name))
18896                        } else {
18897                            e
18898                        }
18899                    })?;
18900                let owned = boxed.materialise().into_owned();
18901                Ok(RangeView::from_owned_rows(owned, self.config.date_system))
18902            }
18903            ReferenceType::Cell3D { .. } | ReferenceType::Range3D { .. } => {
18904                Err(ExcelError::new(ExcelErrorKind::NImpl)
18905                    .with_message("3D references are not yet supported".to_string()))
18906            }
18907        }
18908    }
18909
18910    fn resolve_cell_format(
18911        &self,
18912        sheet: Option<&str>,
18913        row: u32,
18914        col: u32,
18915        current_sheet: &str,
18916    ) -> Option<crate::format::FormatId> {
18917        self.effective_format_id(sheet.unwrap_or(current_sheet), row, col)
18918    }
18919
18920    fn format_class(
18921        &self,
18922        format: crate::format::FormatId,
18923    ) -> Option<formualizer_common::numfmt::FormatClass> {
18924        self.format_registry.class(format).cloned()
18925    }
18926
18927    fn record_cell_derived_format(
18928        &self,
18929        sheet: &str,
18930        row: u32,
18931        col: u32,
18932        format: Option<crate::format::FormatId>,
18933    ) {
18934        if let Some(sheet_id) = self.graph.sheet_id(sheet) {
18935            let cell = CellRef::new(sheet_id, Coord::from_excel(row, col, true, true));
18936            self.record_derived_format_at(cell, format);
18937        }
18938    }
18939
18940    fn resolve_cell_reference_value(
18941        &self,
18942        sheet: Option<&str>,
18943        row: u32,
18944        col: u32,
18945        current_sheet: &str,
18946    ) -> Result<LiteralValue, ExcelError> {
18947        let sheet_name = sheet.unwrap_or(current_sheet);
18948        if self.graph.sheet_id(sheet_name).is_none() {
18949            return Err(ExcelError::new(ExcelErrorKind::Ref));
18950        }
18951        Ok(self
18952            .get_cell_value(sheet_name, row, col)
18953            .unwrap_or(LiteralValue::Empty))
18954    }
18955
18956    fn resolve_cell_reference_value_formatted(
18957        &self,
18958        sheet: Option<&str>,
18959        row: u32,
18960        col: u32,
18961        current_sheet: &str,
18962    ) -> Result<(LiteralValue, Option<crate::format::FormatId>), ExcelError> {
18963        // `resolve_cell_reference_value` + `resolve_cell_format` with one
18964        // sheet lookup of each kind.
18965        let sheet_name = sheet.unwrap_or(current_sheet);
18966        let Some(sheet_id) = self.graph.sheet_id(sheet_name) else {
18967            return Err(ExcelError::new(ExcelErrorKind::Ref));
18968        };
18969        let asheet = self.arrow_sheets.sheet(sheet_name);
18970        Ok(self.read_cell_formatted_in(sheet_id, asheet, row, col))
18971    }
18972
18973    fn build_criteria_mask(
18974        &self,
18975        view: &RangeView<'_>,
18976        col_in_view: usize,
18977        pred: &crate::args::CriteriaPredicate,
18978    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
18979        #[cfg(any(test, feature = "test-support"))]
18980        criteria_mask_test_hooks::note_mask(view.dims().0);
18981        if view.dims().1 == 0 {
18982            return None;
18983        }
18984        // If the view is logically open-ended but the backing sheet has no physical rows,
18985        // treat the mask as empty (0-len) rather than attempting to build a huge mask.
18986        let sheet_rows = view.sheet().nrows as usize;
18987        if sheet_rows == 0 || view.start_row() >= sheet_rows {
18988            return Some(std::sync::Arc::new(arrow_array::BooleanArray::new_null(0)));
18989        }
18990        compute_criteria_mask(view, col_in_view, pred)
18991    }
18992
18993    fn build_row_visibility_mask(
18994        &self,
18995        view: &RangeView<'_>,
18996        mode: VisibilityMaskMode,
18997    ) -> Option<std::sync::Arc<arrow_array::BooleanArray>> {
18998        self.build_row_visibility_mask_for_view(view, mode)
18999    }
19000
19001    fn nested_subtotal_cells(
19002        &self,
19003        view: &RangeView<'_>,
19004        include_aggregate: bool,
19005    ) -> Option<Vec<(usize, usize, usize)>> {
19006        self.nested_subtotal_cells_for_view(view, include_aggregate)
19007    }
19008}
19009
19010impl<R> Engine<R>
19011where
19012    R: EvaluationContext,
19013{
19014    fn clear_spill_projection_and_mirror(
19015        &mut self,
19016        anchor_vertex: VertexId,
19017        delta: Option<&mut DeltaCollector>,
19018    ) {
19019        let spill_cells = self.graph.spill_cells_to_clear(anchor_vertex);
19020        if spill_cells.is_empty() {
19021            return;
19022        }
19023
19024        if let Some(delta) = delta
19025            && delta.mode != DeltaMode::Off
19026        {
19027            let empty = LiteralValue::Empty;
19028            for cell in spill_cells.iter() {
19029                let sheet_name = self.graph.sheet_name(cell.sheet_id);
19030                let old = self
19031                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19032                    .unwrap_or(LiteralValue::Empty);
19033                if old != empty {
19034                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19035                }
19036            }
19037        }
19038
19039        self.graph.clear_spill_region(anchor_vertex);
19040        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
19041            self.record_structural_change(scope);
19042        }
19043
19044        if self.config.arrow_storage_enabled
19045            && self.config.delta_overlay_enabled
19046            && self.config.write_formula_overlay_enabled
19047        {
19048            let empty = LiteralValue::Empty;
19049            for cell in spill_cells.iter() {
19050                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
19051                self.mirror_value_to_computed_overlay(
19052                    &sheet_name,
19053                    cell.coord.row() + 1,
19054                    cell.coord.col() + 1,
19055                    &empty,
19056                );
19057            }
19058        }
19059    }
19060
19061    /// Apply the evaluation outcome for one cyclic SCC: stamp `#CIRC!` on its
19062    /// (optionally filtered) members via `stamp_cycle_error`.
19063    ///
19064    /// This is the single per-SCC application point used by every schedule
19065    /// consumer walking `Schedule::units` (pre-work for #112, where cyclic
19066    /// SCCs will gain runtime verdicts instead of an unconditional stamp).
19067    ///
19068    /// `dirty_filter` preserves the recalc-plan quirk: when `Some(dirty)`,
19069    /// only members present in the set are stamped.
19070    ///
19071    /// Returns the number of vertices stamped (0 when a filter excludes every
19072    /// member), so callers can keep their site-specific `cycle_errors`
19073    /// accounting.
19074    fn apply_cycle_outcome(
19075        &mut self,
19076        cycle: &[VertexId],
19077        mut delta: Option<&mut DeltaCollector>,
19078        dirty_filter: Option<&FxHashSet<VertexId>>,
19079    ) -> usize {
19080        let circ_error = LiteralValue::Error(
19081            ExcelError::new(ExcelErrorKind::Circ)
19082                .with_message("Circular dependency detected".to_string()),
19083        );
19084        let mut stamped = 0usize;
19085        for &vertex_id in cycle {
19086            if let Some(filter) = dirty_filter
19087                && !filter.contains(&vertex_id)
19088            {
19089                continue;
19090            }
19091            self.stamp_cycle_error(vertex_id, &circ_error, delta.as_deref_mut());
19092            stamped += 1;
19093        }
19094        stamped
19095    }
19096
19097    /// Stamp a vertex with `#CIRC!` as part of cycle handling.
19098    ///
19099    /// Unlike a bare `update_vertex_value`, this first tears down any spill the
19100    /// vertex previously anchored: it clears the spilled cells, releases the graph
19101    /// spill registry, drops any lingering region reservation, and mirrors the
19102    /// cleared cells into the computed overlay — the same teardown a normal scalar/
19103    /// error result performs (see `apply_non_array_result_from_parallel` /
19104    /// `clear_spill_projection_and_mirror`). Without this, a #CIRC stamp on a former
19105    /// spill anchor would leave stale spilled values and a reserved region behind
19106    /// (issue #111).
19107    ///
19108    /// When `delta` is provided, the cleared spill cells are recorded (by
19109    /// `clear_spill_projection_and_mirror`) and the anchor's own #CIRC change is
19110    /// recorded here, matching how other result paths emit deltas.
19111    fn stamp_cycle_error(
19112        &mut self,
19113        vertex_id: VertexId,
19114        circ_error: &LiteralValue,
19115        mut delta: Option<&mut DeltaCollector>,
19116    ) {
19117        // Tear down any previous spill projection/region before overwriting the anchor.
19118        if self.graph.spill_registry_has_anchor(vertex_id) {
19119            self.clear_spill_projection_and_mirror(vertex_id, delta.as_deref_mut());
19120        }
19121        // Drop any reservation that was never committed (defensive; normally released
19122        // on the prior successful commit).
19123        self.spill_mgr.release_owner(vertex_id);
19124
19125        // Record the anchor's own #CIRC delta, like other result paths.
19126        if let Some(d) = delta
19127            && d.mode != DeltaMode::Off
19128            && let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id)
19129        {
19130            let sheet_name = self.graph.sheet_name(cell.sheet_id);
19131            let old = self
19132                .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19133                .unwrap_or(LiteralValue::Empty);
19134            if old != *circ_error {
19135                d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19136            }
19137        }
19138
19139        self.graph.update_vertex_value_ref(vertex_id, circ_error);
19140        self.mirror_vertex_value_to_overlay(vertex_id, circ_error);
19141    }
19142
19143    /// Dispatch point for one `ScheduleUnit::Cycle` (RFC #112, Stage 2).
19144    ///
19145    /// * `CycleDetection::Static` — today's behavior, byte-for-byte: stamp
19146    ///   `#CIRC!` on the (optionally dirty-filtered) members.
19147    /// * `CycleDetection::Runtime` — evaluate the SCC via
19148    ///   [`Self::evaluate_scc_unit`]. The recalc-plan dirty quirk maps to:
19149    ///   no dirty member → skip the task entirely (values stand); any dirty
19150    ///   member → the whole SCC evaluates (an SCC cannot be partially
19151    ///   evaluated).
19152    ///
19153    /// Returns the number of `#CIRC!`-stamped vertices, so call sites can
19154    /// keep their `cycle_errors` accounting (`> 0` ⇒ count the unit).
19155    fn handle_cycle_unit(
19156        &mut self,
19157        cycle: &[VertexId],
19158        mut delta: Option<&mut DeltaCollector>,
19159        dirty_filter: Option<&FxHashSet<VertexId>>,
19160        cancel_flag: Option<&AtomicBool>,
19161    ) -> Result<usize, ExcelError> {
19162        self.resource_checkpoint(cycle.len() as u64)?;
19163        match self.config.cycle.detection {
19164            CycleDetection::Static => {
19165                Ok(self.apply_cycle_outcome(cycle, delta.as_deref_mut(), dirty_filter))
19166            }
19167            CycleDetection::Runtime => {
19168                if let Some(filter) = dirty_filter
19169                    && !cycle.iter().any(|v| filter.contains(v))
19170                {
19171                    return Ok(0);
19172                }
19173                // Both policies share `evaluate_scc_unit`; they differ only
19174                // in the settle loop's live-cycle arm (Error stamps,
19175                // Iterate keeps passing — RFC #113).
19176                self.evaluate_scc_unit(cycle, delta, cancel_flag)
19177            }
19178        }
19179    }
19180
19181    /// Evaluate one statically-cyclic SCC under `CycleDetection::Runtime`
19182    /// (design doc `formualizer-stage2-scc-evaluation-design.md` §3; contract
19183    /// spec §3; Iterate policy arm per RFC #113).
19184    ///
19185    /// Phantom SCCs (live-acyclic) produce ordinary values under both
19186    /// policies; live cycles get `#CIRC!` with live-cycle-only blast radius
19187    /// under `CyclePolicy::Error`, or Excel-style iterative calculation
19188    /// (converge per spec §6 or cap at `max_iterations` passes) under
19189    /// `CyclePolicy::Iterate`. Runs sequentially on the
19190    /// coordinating thread; commits are write-through per member (no
19191    /// `ComputedWriteBuffer` — that buffer is scoped to layer evaluation and
19192    /// always flushed before a Cycle unit runs, G1), so later members' scalar
19193    /// *and* range reads observe earlier members' results through the overlay
19194    /// cascade. Deltas are recorded once per member at end of task (G11).
19195    ///
19196    /// Returns the number of vertices stamped `#CIRC!`.
19197    ///
19198    /// `pub(crate)` so tests can drive SCC shapes (e.g. name-vertex members)
19199    /// that ingest-time cycle rejection makes unreachable via public edits.
19200    pub(crate) fn evaluate_scc_unit(
19201        &mut self,
19202        cycle: &[VertexId],
19203        mut delta: Option<&mut DeltaCollector>,
19204        cancel_flag: Option<&AtomicBool>,
19205    ) -> Result<usize, ExcelError> {
19206        struct SccMember {
19207            vertex: VertexId,
19208            cell: Option<CellRef>,
19209        }
19210
19211        let task_start = crate::instant::FzInstant::now();
19212
19213        // ── 0. Member order (spec §7.13): cells ascending (sheet, row, col);
19214        // name vertices after, lexicographic by folded canonical name; any
19215        // other vertex kind (defensive — `get_evaluation_vertices` only emits
19216        // formula/name kinds) last by id, never evaluated.
19217        let mut cell_members: Vec<(VertexId, CellRef)> = Vec::new();
19218        let mut name_members: Vec<(VertexId, String)> = Vec::new();
19219        let mut other_members: Vec<VertexId> = Vec::new();
19220        for &v in cycle {
19221            match self.graph.get_vertex_kind(v) {
19222                VertexKind::FormulaScalar | VertexKind::FormulaArray => {
19223                    match self.graph.get_cell_ref(v) {
19224                        Some(cell) => cell_members.push((v, cell)),
19225                        None => other_members.push(v),
19226                    }
19227                }
19228                VertexKind::NamedScalar | VertexKind::NamedArray => {
19229                    match self.graph.name_key_for_vertex(v) {
19230                        Some(key) => name_members.push((v, key)),
19231                        None => other_members.push(v),
19232                    }
19233                }
19234                _ => other_members.push(v),
19235            }
19236        }
19237        cell_members.sort_unstable_by_key(|(_, c)| (c.sheet_id, c.coord.row(), c.coord.col()));
19238        name_members.sort_unstable_by(|(av, ak), (bv, bk)| ak.cmp(bk).then(av.cmp(bv)));
19239        other_members.sort_unstable();
19240
19241        let cell_refs: Vec<CellRef> = cell_members.iter().map(|(_, c)| *c).collect();
19242        let name_keys: Vec<String> = name_members.iter().map(|(_, k)| k.clone()).collect();
19243        let mut members: Vec<SccMember> = Vec::with_capacity(cycle.len());
19244        for (v, c) in &cell_members {
19245            members.push(SccMember {
19246                vertex: *v,
19247                cell: Some(*c),
19248            });
19249        }
19250        for (v, _) in &name_members {
19251            members.push(SccMember {
19252                vertex: *v,
19253                cell: None,
19254            });
19255        }
19256        for v in &other_members {
19257            members.push(SccMember {
19258                vertex: *v,
19259                cell: None,
19260            });
19261        }
19262        let n = members.len();
19263        // Indices addressable by the collector (cells + names); `other`
19264        // members can be neither edge sources nor targets.
19265        let recordable = cell_refs.len() + name_keys.len();
19266
19267        let circ_error = LiteralValue::Error(
19268            ExcelError::new(ExcelErrorKind::Circ)
19269                .with_message("Circular dependency detected".to_string()),
19270        );
19271
19272        // ── 0b. Spec-§4 persistence repair: structural edits clear computed
19273        // overlays wholesale (`clear_computed_overlay_after_row/_col`), but
19274        // an iterating member's committed value is cycle STATE, not a
19275        // recomputable cache — and in canonical mode the overlay is its ONLY
19276        // home. If the overlay entry vanished since the last recalc, re-seed
19277        // it from the end-of-recalc snapshot (`iterative_state_values`) so
19278        // pass-1 reads (scalar AND range, via the overlay cascade) observe
19279        // the persisted value instead of silently restarting at Empty→0.
19280        // (Found by the iterate edge corpus: inserting/deleting an unrelated
19281        // row reset accumulators, violating spec §4/§7.15.)
19282        if !self.iterative_state_values.is_empty() {
19283            let restore: Vec<(VertexId, LiteralValue)> = members
19284                .iter()
19285                .filter_map(|m| {
19286                    let cell = m.cell?;
19287                    let persisted = self.iterative_state_values.get(&m.vertex)?;
19288                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
19289                    let overlay = self
19290                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19291                        .unwrap_or(LiteralValue::Empty);
19292                    if matches!(overlay, LiteralValue::Empty) {
19293                        Some((m.vertex, persisted.clone()))
19294                    } else {
19295                        None
19296                    }
19297                })
19298                .collect();
19299            for (vertex, value) in restore {
19300                self.mirror_vertex_value_to_overlay(vertex, &value);
19301            }
19302        }
19303
19304        // ── 1. Pre-task value snapshot (overlay-first for cells — G3; the
19305        // graph value map may be evicted in value-cache-disabled mode).
19306        let snapshot: Vec<LiteralValue> = members
19307            .iter()
19308            .map(|m| match m.cell {
19309                Some(cell) => {
19310                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
19311                    self.get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
19312                        .unwrap_or(LiteralValue::Empty)
19313                }
19314                None => self
19315                    .graph
19316                    .get_value(m.vertex)
19317                    .unwrap_or(LiteralValue::Empty),
19318            })
19319            .collect();
19320
19321        // ── 2. Pre-scan: spill anchors (FormulaArray) are stamped `#CIRC!`
19322        // with full spill teardown (spec §7.9, #115) and excluded from
19323        // evaluation. They stay recordable edge TARGETS (readers see `#CIRC!`
19324        // and propagate). Non-evaluable defensive members are excluded too.
19325        let mut excluded = vec![false; n];
19326        let mut last_value = snapshot.clone();
19327        let mut stamped = 0usize;
19328        for (i, m) in members.iter().enumerate() {
19329            match self.graph.get_vertex_kind(m.vertex) {
19330                VertexKind::FormulaArray => {
19331                    // Deltas for the cleared spill-region cells (non-members)
19332                    // can only be recorded here; the anchor's own delta is
19333                    // covered by the end-of-task snapshot comparison (dedup).
19334                    self.stamp_cycle_error(m.vertex, &circ_error, delta.as_deref_mut());
19335                    excluded[i] = true;
19336                    last_value[i] = circ_error.clone();
19337                    stamped += 1;
19338                }
19339                VertexKind::FormulaScalar | VertexKind::NamedScalar | VertexKind::NamedArray => {}
19340                _ => excluded[i] = true,
19341            }
19342        }
19343
19344        let collector = LiveEdgeCollector::new_with_names(&cell_refs, &name_keys);
19345
19346        // Per-member live out-edges, refreshed whenever a member re-runs.
19347        let mut out_edges: Vec<Vec<u32>> = vec![Vec::new(); n];
19348        // Position of each member in the most recent pass (-1 = did not run).
19349        let mut pos: Vec<i64> = vec![-1; n];
19350        // Whether each member's committed value changed in the most recent pass.
19351        let mut changed = vec![false; n];
19352
19353        // Evaluate-and-commit one member; returns Ok(true) when the member was
19354        // stamped `#CIRC!` (array result — would-be spill anchor, spec §7.9).
19355        macro_rules! run_member {
19356            ($i:expr) => {{
19357                let i: usize = $i;
19358                let m = &members[i];
19359                if i < recordable {
19360                    collector.set_current(i as u32);
19361                }
19362                let value = {
19363                    let ctx = RecordingContext::new(&*self, &collector);
19364                    match self.evaluate_vertex_recorded(m.vertex, &ctx, &collector) {
19365                        Ok(v) => v,
19366                        Err(e) => LiteralValue::Error(e),
19367                    }
19368                };
19369                let is_cell_formula = m.cell.is_some();
19370                if is_cell_formula && matches!(value, LiteralValue::Array(_)) {
19371                    // A member that *would* spill inside an SCC gets the
19372                    // conservative §7.9 verdict. It has never spilled before
19373                    // (a prior spill would make it FormulaArray, pre-stamped
19374                    // above), so there is no projection to tear down.
19375                    self.stamp_cycle_error(m.vertex, &circ_error, None);
19376                    excluded[i] = true;
19377                    stamped += 1;
19378                    changed[i] = last_value[i] != circ_error;
19379                    last_value[i] = circ_error.clone();
19380                } else {
19381                    self.graph.update_vertex_value_ref(m.vertex, &value);
19382                    self.mirror_vertex_value_to_overlay(m.vertex, &value);
19383                    // §7.14 invariant (G2): a formula member must never be
19384                    // shadowed by a user/delta overlay entry, or iteration
19385                    // reads would silently diverge from committed values.
19386                    #[cfg(debug_assertions)]
19387                    if let Some(cell) = m.cell {
19388                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
19389                        debug_assert!(
19390                            self.read_delta_overlay_cell(
19391                                &sheet_name,
19392                                cell.coord.row() + 1,
19393                                cell.coord.col() + 1
19394                            )
19395                            .is_none(),
19396                            "user overlay must never shadow a formula SCC member ({sheet_name}!r{}c{})",
19397                            cell.coord.row() + 1,
19398                            cell.coord.col() + 1
19399                        );
19400                    }
19401                    changed[i] = last_value[i] != value;
19402                    last_value[i] = value;
19403                }
19404            }};
19405        }
19406
19407        let check_cancel = |flag: Option<&AtomicBool>| -> Result<(), ExcelError> {
19408            if let Some(flag) = flag
19409                && flag.load(Ordering::Relaxed)
19410            {
19411                return Err(ExcelError::new(ExcelErrorKind::Cancelled)
19412                    .with_message("Evaluation cancelled during SCC evaluation".to_string()));
19413            }
19414            Ok(())
19415        };
19416
19417        // ── 3. Pass 1: all evaluable members in member order.
19418        check_cancel(cancel_flag)?;
19419        let mut passes = 1usize;
19420        {
19421            let mut p = 0i64;
19422            for i in 0..n {
19423                if excluded[i] {
19424                    continue;
19425                }
19426                run_member!(i);
19427                pos[i] = p;
19428                p += 1;
19429            }
19430        }
19431
19432        // ── 4. Settle loop (design doc §3 step 4; RFC #113 policy arm).
19433        //
19434        // Acyclic classifications settle stale readers exactly (identical
19435        // under both policies — phantom SCCs never iterate). A witnessed
19436        // live cycle dispatches on policy: `Error` stamps `#CIRC!` and
19437        // stops; `Iterate` keeps running full passes over all members in
19438        // member order until converged (spec §6) or capped at
19439        // `max_iterations` total passes. A live cycle that only appears
19440        // mid-settle takes the same arm, and a cycle that dissolves
19441        // mid-iteration falls back to exact acyclic settling.
19442        //
19443        // Defensive acyclic budget: the acyclic settle is monotone, so more
19444        // than |SCC| + 2 settle passes can only be a bug; cap hits stamp the
19445        // remainder and set telemetry. Tracked via `settle_passes` so
19446        // iteration passes (legitimately many) don't consume the budget.
19447        let policy = self.config.cycle.policy;
19448        let cap = n + 2;
19449        let mut witnessed_cycles = 0usize;
19450        let mut capped = false;
19451        // ── Iterate-policy state ──
19452        let mut iterating = false;
19453        let mut converged = false;
19454        let mut exact_fixed_point = false;
19455        // Values committed by the last *full* pass; `None` until the first
19456        // iteration pass runs (pass 1 has no predecessor to compare against)
19457        // and reset when a settle pass runs (no cross-kind comparisons).
19458        let mut prev_pass: Option<Vec<LiteralValue>> = None;
19459        // Final-round convergence stats (overwritten per round so the values
19460        // reported are the ones observed at stop).
19461        let mut iter_max_delta = 0f64;
19462        let mut iter_nan_converged = 0usize;
19463        // Acyclic stale-reader re-eval passes (defensive budget; under pure
19464        // Error flow `1 + settle_passes == passes`, preserving Stage-2
19465        // behavior exactly).
19466        let mut settle_passes = 0usize;
19467        loop {
19468            // Drain this pass's recordings; members that ran replace their
19469            // out-edge set, members that didn't keep last-known edges.
19470            let drained = collector.take_edges();
19471            for i in 0..n {
19472                if pos[i] >= 0 {
19473                    out_edges[i].clear();
19474                }
19475            }
19476            for (from, to) in drained {
19477                debug_assert!(
19478                    pos[from as usize] >= 0,
19479                    "edge from a member that did not run"
19480                );
19481                out_edges[from as usize].push(to);
19482            }
19483            let mut edges: Vec<(u32, u32)> = Vec::new();
19484            for (i, outs) in out_edges.iter().enumerate() {
19485                if excluded[i] {
19486                    continue;
19487                }
19488                for &t in outs {
19489                    edges.push((i as u32, t));
19490                }
19491            }
19492            edges.sort_unstable();
19493            edges.dedup();
19494
19495            let analysis = analyze_live_graph(n, &edges);
19496
19497            if analysis.cycle_count > 0 {
19498                // Classification repeats every iteration pass under
19499                // `Iterate`; record the widest single witness instead of
19500                // accumulating so the count stays "distinct live cycles".
19501                witnessed_cycles = witnessed_cycles.max(analysis.cycle_count);
19502                match policy {
19503                    CyclePolicy::Error => {
19504                        // POLICY (Error): stamp every member of a live cycle,
19505                        // then one settling pass over the remaining members in
19506                        // live-topological order so error propagation
19507                        // downstream is consistent (spec §3.4). Blast radius =
19508                        // live cycles only.
19509                        for i in 0..n {
19510                            if analysis.in_cycle[i] && !excluded[i] {
19511                                self.stamp_cycle_error(members[i].vertex, &circ_error, None);
19512                                excluded[i] = true;
19513                                last_value[i] = circ_error.clone();
19514                                stamped += 1;
19515                            }
19516                        }
19517                        check_cancel(cancel_flag)?;
19518                        let order: Vec<usize> = analysis
19519                            .topo
19520                            .iter()
19521                            .map(|&i| i as usize)
19522                            .filter(|&i| !excluded[i])
19523                            .collect();
19524                        if !order.is_empty() {
19525                            passes += 1;
19526                            for i in order {
19527                                run_member!(i);
19528                            }
19529                        }
19530                        break;
19531                    }
19532                    CyclePolicy::Iterate {
19533                        max_iterations,
19534                        max_change,
19535                    } => {
19536                        // POLICY (Iterate), spec §3.5/§6.
19537                        iterating = true;
19538
19539                        // Convergence test: the full pass that just completed
19540                        // vs the previous full pass, per the spec-§6 rules.
19541                        // `prev_pass` is `None` until an iteration pass has
19542                        // run — pass 1 has no predecessor, so no convergence
19543                        // test occurs before the second pass (spec §6).
19544                        if let Some(prev) = &prev_pass {
19545                            let mut round_max_delta = 0f64;
19546                            let mut round_nan = 0usize;
19547                            let mut all_converged = true;
19548                            let mut round_exact = true;
19549                            for i in 0..n {
19550                                if excluded[i] {
19551                                    // Stamped mid-iteration (array result,
19552                                    // §7.9): the value is pinned and cannot
19553                                    // change again — trivially settled.
19554                                    continue;
19555                                }
19556                                let out = crate::engine::convergence::values_converged(
19557                                    &prev[i],
19558                                    &last_value[i],
19559                                    max_change,
19560                                    self.config.date_system,
19561                                );
19562                                if out.nan_converged {
19563                                    round_nan += 1;
19564                                }
19565                                if let Some(d) = out.abs_delta {
19566                                    round_max_delta = round_max_delta.max(d);
19567                                    if d != 0.0 {
19568                                        round_exact = false;
19569                                    }
19570                                }
19571                                if !out.converged {
19572                                    all_converged = false;
19573                                }
19574                            }
19575                            // Overwrite (not max): telemetry reports the
19576                            // round observed at stop.
19577                            iter_max_delta = round_max_delta;
19578                            iter_nan_converged = round_nan;
19579                            if all_converged {
19580                                exact_fixed_point = round_exact;
19581                                converged = true;
19582                                break;
19583                            }
19584                        }
19585
19586                        // ── Pass-counting reconciliation (spec §6/§7.6):
19587                        // `max_iterations` counts TOTAL passes, pass 1
19588                        // included, and pass 1 has already run by the time a
19589                        // live cycle is first witnessed here. The budget is
19590                        // therefore checked BEFORE evaluating anything more:
19591                        // with `max_iterations: 1` we stop right here — each
19592                        // member was evaluated exactly once this recalc (the
19593                        // Excel accumulator contract) and no convergence test
19594                        // ran (`prev_pass` is still `None`). Capping keeps
19595                        // the last committed values and is NOT an error
19596                        // (Excel parity); telemetry records it.
19597                        if passes >= max_iterations as usize {
19598                            capped = true;
19599                            break;
19600                        }
19601
19602                        check_cancel(cancel_flag)?;
19603                        // One more full pass over every evaluable member in
19604                        // member order (Gauss–Seidel: each commit is visible
19605                        // to later members within the pass). Live edges
19606                        // re-record — guards can flip near convergence
19607                        // (§7.3) — so classification repeats next time
19608                        // around, and a cycle that dissolves drops back to
19609                        // the exact acyclic settle below.
19610                        prev_pass = Some(last_value.clone());
19611                        for x in pos.iter_mut() {
19612                            *x = -1;
19613                        }
19614                        changed.fill(false);
19615                        passes += 1;
19616                        let mut p = 0i64;
19617                        for i in 0..n {
19618                            if excluded[i] {
19619                                continue;
19620                            }
19621                            run_member!(i);
19622                            pos[i] = p;
19623                            p += 1;
19624                        }
19625                        continue;
19626                    }
19627                }
19628            }
19629
19630            // Acyclic: find stale readers — members whose live read of `to`
19631            // happened before `to`'s value changed in the pass that just ran.
19632            let mut stale: Vec<usize> = Vec::new();
19633            for i in 0..n {
19634                if excluded[i] {
19635                    continue;
19636                }
19637                let is_stale = out_edges[i].iter().any(|&t| {
19638                    let t = t as usize;
19639                    changed[t] && (pos[i] < 0 || (pos[t] >= 0 && pos[i] < pos[t]))
19640                });
19641                if is_stale {
19642                    stale.push(i);
19643                }
19644            }
19645            if stale.is_empty() {
19646                break; // values exact — phantom SCC (or dissolved live cycle)
19647            }
19648            if 1 + settle_passes >= cap {
19649                // Defensive only; hitting this is a bug (loud telemetry).
19650                capped = true;
19651                for (i, m) in members.iter().enumerate() {
19652                    if !excluded[i] {
19653                        self.stamp_cycle_error(m.vertex, &circ_error, None);
19654                        excluded[i] = true;
19655                        last_value[i] = circ_error.clone();
19656                        stamped += 1;
19657                    }
19658                }
19659                break;
19660            }
19661
19662            check_cancel(cancel_flag)?;
19663            // Re-evaluate stale readers in live-topo order, recording fresh
19664            // edges (branches may flip on re-eval — spec §7.3 — which is why
19665            // classification repeats).
19666            // A settle pass is a partial sweep: drop the full-pass baseline
19667            // so a live cycle (re)appearing afterwards never compares values
19668            // across mixed pass kinds.
19669            prev_pass = None;
19670            let topo_pos = analysis.topo_positions();
19671            stale.sort_unstable_by_key(|&i| topo_pos[i]);
19672            for x in pos.iter_mut() {
19673                *x = -1;
19674            }
19675            changed.fill(false);
19676            passes += 1;
19677            settle_passes += 1;
19678            for (p, i) in stale.into_iter().enumerate() {
19679                run_member!(i);
19680                pos[i] = p as i64;
19681            }
19682        }
19683
19684        // Post-work boundary: members are committed write-through, so a
19685        // member that ran across a live cancellation is already visible; the
19686        // task still must not complete (no delta, retention or iteration
19687        // state). The members stay dirty for the retry, as with the
19688        // mid-task checks above.
19689        self.live_cancellation_after_work(SCC_CANCELLED)?;
19690
19691        // Iteration that ended because the live cycle dissolved and the
19692        // acyclic settle reached exactness counts as converged (values are
19693        // exact, strictly better than threshold-converged). The defensive
19694        // settle cap (`capped` + stamping) is not.
19695        if iterating && !converged && !capped {
19696            converged = true;
19697        }
19698
19699        // ── 5. End of task: one delta per member whose final value differs
19700        // from the pre-task snapshot (spec §3 side-effect rule, G11).
19701        collector.clear_current();
19702        if let Some(d) = delta
19703            && d.mode != DeltaMode::Off
19704        {
19705            for (i, m) in members.iter().enumerate() {
19706                if let Some(cell) = m.cell
19707                    && last_value[i] != snapshot[i]
19708                {
19709                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
19710                }
19711            }
19712        }
19713
19714        // Members of an SCC that iterated re-evaluate on EVERY recalc, like
19715        // Excel's circular cells: register them for the end-of-recalc
19716        // volatile-like redirty (see `pending_iterative_redirty`). Marking
19717        // any one member propagates around the (strongly connected) SCC and
19718        // to downstream dependents, but all members are registered so the
19719        // contract survives partial structural edits between recalcs.
19720        //
19721        // Exception (#368): an SCC that stopped on an
19722        // exact fixed point — every member reproduced its previous value
19723        // bit-for-bit before the pass cap, no NaN identity, no volatile or
19724        // dynamic-reference member — cannot change on a re-run with the same
19725        // inputs, so it is retained clean. The dirty graph decides when it
19726        // runs again. Tolerance-only convergence (|Δ| < max_change but ≠ 0),
19727        // capped SCCs (including the `max_iterations: 1` accumulator
19728        // contract) and volatile cycles keep the per-recalc redirty.
19729        // Whatever the outcome, this task supersedes any earlier retention
19730        // of its members.
19731        if !self.retained_scc_members.is_empty() {
19732            for m in members.iter() {
19733                self.retained_scc_members.remove(&m.vertex);
19734            }
19735        }
19736        if iterating {
19737            let retain = converged
19738                && !capped
19739                && exact_fixed_point
19740                && iter_nan_converged == 0
19741                && members
19742                    .iter()
19743                    .all(|m| !self.graph.is_volatile(m.vertex) && !self.graph.is_dynamic(m.vertex));
19744            if retain {
19745                if self.retained_scc_members.is_empty() {
19746                    self.retained_scc_config_fingerprint = self.retained_scc_config_fingerprint();
19747                    self.retained_scc_function_epoch_seen =
19748                        crate::function_registry::semantic_epoch();
19749                    self.retained_scc_provider_revision_seen =
19750                        self.resolver.planning_semantic_revision();
19751                }
19752                let scc_id = self.next_retained_scc_id;
19753                self.next_retained_scc_id = self.next_retained_scc_id.wrapping_add(1);
19754                for (i, m) in members.iter().enumerate() {
19755                    self.retained_scc_members.insert(m.vertex, scc_id);
19756                    // §4 persistence snapshot, written once: retained
19757                    // members do not pass through `redirty_for_next_recalc`.
19758                    if matches!(last_value[i], LiteralValue::Empty) {
19759                        self.iterative_state_values.remove(&m.vertex);
19760                    } else {
19761                        self.iterative_state_values
19762                            .insert(m.vertex, last_value[i].clone());
19763                    }
19764                }
19765            } else {
19766                self.pending_iterative_redirty
19767                    .extend(members.iter().map(|m| m.vertex));
19768            }
19769        } else if !self.iterative_state_values.is_empty() {
19770            // The cycle dissolved (phantom settle or `#CIRC!` stamping):
19771            // these members are ordinary formulas again and must not carry
19772            // stale iteration state into a future cycle.
19773            for m in members.iter() {
19774                self.iterative_state_values.remove(&m.vertex);
19775            }
19776        }
19777
19778        {
19779            let t = &mut self.last_cycle_telemetry;
19780            t.static_sccs += 1;
19781            if witnessed_cycles == 0 && stamped == 0 && !capped {
19782                t.phantom_sccs += 1;
19783            }
19784            t.live_cycles_witnessed += witnessed_cycles;
19785            t.circ_cells_stamped += stamped;
19786            t.settle_passes_total += passes;
19787            t.max_passes_single_scc = t.max_passes_single_scc.max(passes);
19788            if iterating {
19789                t.iterated_sccs += 1;
19790                if converged {
19791                    t.converged_sccs += 1;
19792                }
19793                t.max_abs_delta_at_stop = t.max_abs_delta_at_stop.max(iter_max_delta);
19794                t.nan_converged += iter_nan_converged;
19795            }
19796            if capped {
19797                t.capped_sccs += 1;
19798            }
19799            t.elapsed_ms += task_start.elapsed().as_millis();
19800        }
19801
19802        Ok(stamped)
19803    }
19804
19805    /// Recorded sibling of [`Self::evaluate_vertex_immutable`]: evaluates one
19806    /// SCC member's AST via an [`Interpreter`] over a [`RecordingContext`] so
19807    /// reads that actually occur are captured as live edges. Value semantics
19808    /// must match `evaluate_vertex_immutable` exactly (including the missing-
19809    /// AST `Number(0.0)` quirk, G14); named Cell/Range/Literal definitions
19810    /// delegate to it after recording the definition region by hand (those
19811    /// reads bypass the context).
19812    fn evaluate_vertex_recorded(
19813        &self,
19814        vertex_id: VertexId,
19815        ctx: &RecordingContext<'_, R>,
19816        collector: &LiveEdgeCollector,
19817    ) -> Result<LiteralValue, ExcelError> {
19818        if !self.graph.vertex_exists(vertex_id) {
19819            return Err(ExcelError::new(formualizer_common::ExcelErrorKind::Ref)
19820                .with_message(format!("Vertex not found: {vertex_id:?}")));
19821        }
19822
19823        let kind = self.graph.get_vertex_kind(vertex_id);
19824        let sheet_id = self.graph.get_vertex_sheet_id(vertex_id);
19825
19826        match kind {
19827            VertexKind::FormulaScalar | VertexKind::FormulaArray => {
19828                let Some(view) = self.graph.formula_view(vertex_id) else {
19829                    return Ok(LiteralValue::Number(0.0)); // G14 quirk
19830                };
19831                let sheet_name = self.graph.sheet_name(sheet_id);
19832                let cell_ref = self
19833                    .graph
19834                    .get_cell_ref(vertex_id)
19835                    .expect("cell ref for vertex");
19836                let interpreter = Interpreter::new_with_cell(ctx, sheet_name, cell_ref);
19837                interpreter
19838                    .evaluate_formula_view(view, self.graph.data_store(), self.graph.sheet_reg())
19839                    .map(|cv| {
19840                        let format = cv.format_id();
19841                        self.record_derived_format(vertex_id, format);
19842                        self.materialize_formula_result(vertex_id, cv)
19843                    })
19844                    .or_else(|error| self.fit_formula_error(vertex_id, error))
19845            }
19846            VertexKind::NamedScalar | VertexKind::NamedArray => {
19847                let named_range = self.graph.named_range_by_vertex(vertex_id).ok_or_else(|| {
19848                    ExcelError::new(ExcelErrorKind::Name)
19849                        .with_message("Named range metadata missing".to_string())
19850                })?;
19851
19852                match &named_range.definition {
19853                    NamedDefinition::Formula { ast, .. } => {
19854                        let context_sheet = match named_range.scope {
19855                            NameScope::Sheet(id) => id,
19856                            NameScope::Workbook => sheet_id,
19857                        };
19858                        let sheet_name = self.graph.sheet_name(context_sheet);
19859                        let cell_ref = self
19860                            .graph
19861                            .get_cell_ref(vertex_id)
19862                            .unwrap_or_else(|| self.graph.make_cell_ref(sheet_name, 0, 0));
19863                        let interpreter = Interpreter::new_with_cell(ctx, sheet_name, cell_ref);
19864                        if kind == VertexKind::NamedScalar {
19865                            interpreter.evaluate_ast(ast).map(|cv| cv.into_literal())
19866                        } else {
19867                            match interpreter.evaluate_ast(ast) {
19868                                Ok(cv) => match cv.into_literal() {
19869                                    v @ LiteralValue::Array(_) => Ok(v),
19870                                    other => Ok(LiteralValue::Array(vec![vec![other]])),
19871                                },
19872                                Err(err) => Ok(LiteralValue::Error(err)),
19873                            }
19874                        }
19875                    }
19876                    NamedDefinition::Cell(cell_ref) => {
19877                        // The definition is read via direct grid access in
19878                        // `evaluate_vertex_immutable`; record the live edge
19879                        // by hand before delegating.
19880                        collector.record_scalar(
19881                            cell_ref.sheet_id,
19882                            cell_ref.coord.row(),
19883                            cell_ref.coord.col(),
19884                        );
19885                        self.evaluate_vertex_immutable(vertex_id)
19886                    }
19887                    NamedDefinition::Range(range_ref) => {
19888                        if range_ref.start.sheet_id == range_ref.end.sheet_id {
19889                            collector.record_rect(
19890                                range_ref.start.sheet_id,
19891                                range_ref.start.coord.row(),
19892                                range_ref.start.coord.col(),
19893                                range_ref.end.coord.row(),
19894                                range_ref.end.coord.col(),
19895                            );
19896                        }
19897                        self.evaluate_vertex_immutable(vertex_id)
19898                    }
19899                    NamedDefinition::Literal(_) => self.evaluate_vertex_immutable(vertex_id),
19900                }
19901            }
19902            _ => self.evaluate_vertex_immutable(vertex_id),
19903        }
19904    }
19905
19906    /// Pending source occupancy is independent of formula preparation and value caches.
19907    fn pending_spill_occupied(&self, anchor: CellRef, end_row: u32, end_col: u32) -> bool {
19908        let sheet = self.graph.sheet_name(anchor.sheet_id);
19909        let package = self
19910            .staged_formulas
19911            .get(sheet)
19912            .and_then(|staged| staged.deferred_package.as_ref());
19913        self.staged_formula_index.occupies_spill(
19914            sheet,
19915            (anchor.coord.row() + 1, anchor.coord.col() + 1),
19916            (end_row + 1, end_col + 1),
19917            |point| package.is_some_and(|package| package.suppressed.contains(&point)),
19918        )
19919    }
19920
19921    fn remember_pending_spill(
19922        &mut self,
19923        vertex: VertexId,
19924        anchor: CellRef,
19925        region: Region,
19926    ) -> Result<(), ExcelError> {
19927        self.cancellation_checkpoint("pending spill occupancy")?;
19928        self.resource_checkpoint(1)?;
19929        if let Some(entry) = self
19930            .blocked_pending_spills
19931            .iter_mut()
19932            .find(|entry| entry.0 == vertex)
19933        {
19934            *entry = (vertex, anchor, region);
19935            return Ok(());
19936        }
19937        if self.blocked_pending_spills.len() == self.blocked_pending_spills.capacity() {
19938            // Geometric growth avoids copying every existing retry entry for
19939            // every new blocked anchor. Admit the entire capacity increment.
19940            let additional = self.blocked_pending_spills.capacity().max(1);
19941            let bytes =
19942                (additional as u64)
19943                    .saturating_mul(std::mem::size_of::<(VertexId, CellRef, Region)>() as u64);
19944            if let Some(ledger) = self.active_resource_ledger.as_mut() {
19945                ledger
19946                    .reserve_retained(bytes)
19947                    .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
19948                self.source_cache_accounted = self.source_cache_accounted.saturating_add(bytes);
19949            }
19950            if self
19951                .blocked_pending_spills
19952                .try_reserve_exact(additional)
19953                .is_err()
19954            {
19955                if let Some(ledger) = self.active_resource_ledger.as_mut() {
19956                    ledger
19957                        .release_retained(bytes)
19958                        .map_err(crate::engine::ResourceLedgerError::into_excel_error)?;
19959                    self.source_cache_accounted -= bytes;
19960                }
19961                return Err(crate::engine::ResourceLedgerError::Exhausted(
19962                    formualizer_common::ResourceExhaustionDetail {
19963                        reason: formualizer_common::ResourceExhaustionReason::RetainedMemory,
19964                        limit: u64::MAX,
19965                        observed: bytes,
19966                        request_id: None,
19967                    },
19968                )
19969                .into_excel_error());
19970            }
19971        }
19972        self.blocked_pending_spills.push((vertex, anchor, region));
19973        Ok(())
19974    }
19975
19976    // Successful edits wake only intersecting attempted regions. Keep the entry
19977    // until evaluation: logged edits may still roll back, and materializing a
19978    // pending formula must not destroy its anchor's occupancy retry information.
19979    fn invalidate_pending_spills(&mut self, scope: StructuralScope) {
19980        if let StructuralScope::RemovedSheet(sheet) = scope {
19981            self.blocked_pending_spills
19982                .retain(|entry| entry.1.sheet_id != sheet);
19983            return;
19984        }
19985        for &(vertex, anchor, region) in &self.blocked_pending_spills {
19986            let affected = match scope {
19987                StructuralScope::Cell { sheet, row, col } => {
19988                    region.intersects(&Region::point(sheet, row, col))
19989                }
19990                StructuralScope::Region(changed) => region.intersects(&changed),
19991                StructuralScope::Sheet(sheet) | StructuralScope::RemovedSheet(sheet) => {
19992                    region.sheet_id() == sheet
19993                }
19994                StructuralScope::OpaqueGlobal | StructuralScope::AllSheets => true,
19995            };
19996            if affected
19997                && self.graph.vertex_exists(vertex)
19998                && self.graph.get_cell_ref(vertex) == Some(anchor)
19999                && matches!(
20000                    self.graph.get_vertex_kind(vertex),
20001                    VertexKind::FormulaScalar | VertexKind::FormulaArray
20002                )
20003            {
20004                self.graph.mark_vertex_dirty(vertex);
20005            }
20006        }
20007    }
20008
20009    fn guard_pending_spill_commit(
20010        &mut self,
20011        anchor_vertex: VertexId,
20012        targets: &[CellRef],
20013    ) -> Result<(), ExcelError> {
20014        let Some(anchor) = self.graph.get_cell_ref(anchor_vertex) else {
20015            return Ok(());
20016        };
20017        let Some(last) = targets.last() else {
20018            return Ok(());
20019        };
20020        let occupied = self.pending_spill_occupied(anchor, last.coord.row(), last.coord.col())
20021            || self
20022                .graph
20023                .table_intersects_spill(anchor, last.coord.row(), last.coord.col());
20024        if (occupied
20025            || self
20026                .blocked_pending_spills
20027                .iter()
20028                .any(|entry| entry.0 == anchor_vertex))
20029            && let Err(error) = self.remember_pending_spill(
20030                anchor_vertex,
20031                anchor,
20032                Region::rect(
20033                    anchor.sheet_id,
20034                    anchor.coord.row(),
20035                    last.coord.row(),
20036                    anchor.coord.col(),
20037                    last.coord.col(),
20038                ),
20039            )
20040        {
20041            self.spill_mgr.release_owner(anchor_vertex);
20042            return Err(error);
20043        }
20044        if occupied {
20045            self.spill_mgr.release_owner(anchor_vertex);
20046            return Err(ExcelError::new(ExcelErrorKind::Spill)
20047                .with_message("Spill blocked")
20048                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
20049                    expected_rows: last.coord.row() - anchor.coord.row() + 1,
20050                    expected_cols: last.coord.col() - anchor.coord.col() + 1,
20051                }));
20052        }
20053        Ok(())
20054    }
20055
20056    /// Commit spill via shim and mirror resulting cells into Arrow overlay.
20057    fn commit_spill_and_mirror(
20058        &mut self,
20059        anchor_vertex: VertexId,
20060        targets: &[CellRef],
20061        rows: Vec<Vec<LiteralValue>>,
20062        delta: Option<&mut DeltaCollector>,
20063    ) -> Result<(), ExcelError> {
20064        self.guard_pending_spill_commit(anchor_vertex, targets)?;
20065        let prev_spill_cells = self.graph.spill_cells_to_clear(anchor_vertex);
20066
20067        if let Some(delta) = delta
20068            && delta.mode != DeltaMode::Off
20069        {
20070            let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
20071                targets.iter().copied().collect();
20072            let empty = LiteralValue::Empty;
20073
20074            // Clears (prev - targets)
20075            for cell in prev_spill_cells.iter() {
20076                if target_set.contains(cell) {
20077                    continue;
20078                }
20079                let sheet_name = self.graph.sheet_name(cell.sheet_id);
20080                let old = self
20081                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20082                    .unwrap_or(LiteralValue::Empty);
20083                if old != empty {
20084                    delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
20085                }
20086            }
20087
20088            // Writes (targets)
20089            if !targets.is_empty() && !rows.is_empty() && !rows[0].is_empty() {
20090                let width = rows[0].len();
20091                for (idx, cell) in targets.iter().enumerate() {
20092                    let r_off = idx / width;
20093                    let c_off = idx % width;
20094                    let new = rows
20095                        .get(r_off)
20096                        .and_then(|r| r.get(c_off))
20097                        .cloned()
20098                        .unwrap_or(LiteralValue::Empty);
20099                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
20100                    let old = self
20101                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20102                        .unwrap_or(LiteralValue::Empty);
20103                    if old != new {
20104                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
20105                    }
20106                }
20107            } else {
20108                // Degenerate shapes: if we have targets but no rows, treat as writing Empty.
20109                for cell in targets.iter() {
20110                    let sheet_name = self.graph.sheet_name(cell.sheet_id);
20111                    let old = self
20112                        .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20113                        .unwrap_or(LiteralValue::Empty);
20114                    if !matches!(old, LiteralValue::Empty) {
20115                        delta.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
20116                    }
20117                }
20118            }
20119        }
20120
20121        // Commit via shim (releases locks). When the graph value cache is disabled (Arrow-canonical
20122        // values), plan/commit must consult Arrow storage to detect non-empty value blockers.
20123        let arrow_sheets = &self.arrow_sheets;
20124        self.spill_mgr.commit_array_with_value_probe(
20125            &mut self.graph,
20126            anchor_vertex,
20127            targets,
20128            rows.clone(),
20129            |g, cell| {
20130                let sheet_name = g.sheet_name(cell.sheet_id);
20131                let asheet = arrow_sheets.sheet(sheet_name)?;
20132                let r0 = cell.coord.row() as usize;
20133                let c0 = cell.coord.col() as usize;
20134                let v = asheet.get_cell_value(r0, c0);
20135                if matches!(v, LiteralValue::Empty) {
20136                    None
20137                } else {
20138                    Some(v)
20139                }
20140            },
20141        )?;
20142
20143        self.blocked_pending_spills
20144            .retain(|entry| entry.0 != anchor_vertex);
20145        self.spill_blocker_waiters.forget(anchor_vertex);
20146        if let Some(scope) = Self::structural_scope_from_cells(&prev_spill_cells) {
20147            self.record_structural_change(scope);
20148        }
20149        if let Some(scope) = Self::structural_scope_from_cells(targets) {
20150            self.record_structural_change(scope);
20151        }
20152
20153        if self.config.arrow_storage_enabled
20154            && self.config.delta_overlay_enabled
20155            && self.config.write_formula_overlay_enabled
20156        {
20157            if !prev_spill_cells.is_empty() {
20158                let target_set: std::collections::HashSet<CellRef, CoordBuildHasher> =
20159                    targets.iter().copied().collect();
20160                let empty = LiteralValue::Empty;
20161                for cell in prev_spill_cells.iter() {
20162                    if !target_set.contains(cell) {
20163                        let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
20164                        self.mirror_value_to_computed_overlay(
20165                            &sheet_name,
20166                            cell.coord.row() + 1,
20167                            cell.coord.col() + 1,
20168                            &empty,
20169                        );
20170                    }
20171                }
20172            }
20173
20174            for (idx, cell) in targets.iter().enumerate() {
20175                if rows.is_empty() || rows[0].is_empty() {
20176                    break;
20177                }
20178                let width = rows[0].len();
20179                let r_off = idx / width;
20180                let c_off = idx % width;
20181                let v = rows[r_off][c_off].clone();
20182                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
20183                self.mirror_value_to_computed_overlay(
20184                    &sheet_name,
20185                    cell.coord.row() + 1,
20186                    cell.coord.col() + 1,
20187                    &v,
20188                );
20189            }
20190        }
20191        Ok(())
20192    }
20193}
20194
20195#[cfg(test)]
20196#[path = "tests/authority_schedule_execution.rs"]
20197mod authority_schedule_execution;
20198
20199#[cfg(test)]
20200#[path = "tests/pending_spill.rs"]
20201mod pending_spill_tests;
20202
20203#[cfg(test)]
20204#[path = "tests/spill_batch_abort_192.rs"]
20205mod spill_batch_abort_192;
20206
20207// ── Effects pipeline (ticket 603) ──────────────────────────────────────────
20208//
20209// Compute → Plan → Apply separation for evaluation side-effects.
20210
20211use crate::engine::effects::Effect;
20212use crate::engine::graph::editor::change_log::{ChangeEvent, ChangeLog, SpillSnapshot};
20213
20214impl<R> Engine<R>
20215where
20216    R: EvaluationContext,
20217{
20218    /// Plan effects for a single vertex after its value has been computed.
20219    ///
20220    /// This reads graph state but only performs lightweight mutations
20221    /// (`set_kind`, `spill_mgr.reserve`) that are needed for correctness
20222    /// during the planning phase.  Value-changing mutations are deferred to
20223    /// `apply_effect`.
20224    pub(crate) fn plan_vertex_effects(
20225        &mut self,
20226        vertex_id: VertexId,
20227        computed_value: LiteralValue,
20228    ) -> Result<Vec<Effect>, ExcelError> {
20229        // FR3: a stale dynamic reader publishes nothing; FR2: anything else
20230        // leaves the dirty set at its commit (design §8.2).
20231        if self.freshness_armed() {
20232            if self.freshness_drop_stale(vertex_id) {
20233                return Ok(Vec::new());
20234            }
20235            let effects = self.plan_vertex_effects_unrecorded(vertex_id, computed_value)?;
20236            self.freshness_mark_committed(vertex_id);
20237            return Ok(effects);
20238        }
20239        self.plan_vertex_effects_unrecorded(vertex_id, computed_value)
20240    }
20241
20242    fn plan_vertex_effects_unrecorded(
20243        &mut self,
20244        vertex_id: VertexId,
20245        computed_value: LiteralValue,
20246    ) -> Result<Vec<Effect>, ExcelError> {
20247        let kind = self.graph.get_vertex_kind(vertex_id);
20248        let is_formula = matches!(kind, VertexKind::FormulaScalar | VertexKind::FormulaArray);
20249
20250        // If this vertex's cell is currently covered by a spill from a different
20251        // anchor, ignore the computed result.  Formula vertices are exempt:
20252        // they must still evaluate so that overlapping spills produce #SPILL!.
20253        if !is_formula {
20254            if let Some(cell) = self.graph.get_cell_ref(vertex_id)
20255                && let Some(owner) = self.graph.spill_registry_anchor_for_cell(cell)
20256                && owner != vertex_id
20257            {
20258                return Ok(Vec::new());
20259            }
20260            // Non-formula vertices: store value as-is (arrays remain arrays; no spill).
20261            return Ok(vec![Effect::WriteCell {
20262                vertex_id,
20263                value: computed_value,
20264            }]);
20265        }
20266
20267        match computed_value {
20268            LiteralValue::Array(rows) => self.plan_array_effects(vertex_id, rows),
20269            other => self.plan_scalar_effects(vertex_id, other),
20270        }
20271    }
20272
20273    /// Plan effects for a formula vertex that produced a scalar/error result.
20274    fn plan_scalar_effects(
20275        &mut self,
20276        vertex_id: VertexId,
20277        value: LiteralValue,
20278    ) -> Result<Vec<Effect>, ExcelError> {
20279        // Range admission substitutes the same cap error before allocating rows.
20280        // Preserve the owned-array rejection's kind, release and clear effects.
20281        if let LiteralValue::Error(error) = &value
20282            && error.kind == ExcelErrorKind::Spill
20283            && error.message.as_deref() == Some("SpillTooLarge")
20284            && let formualizer_common::ExcelErrorExtra::Spill {
20285                expected_rows,
20286                expected_cols,
20287            } = error.extra
20288            && u64::from(expected_rows).saturating_mul(u64::from(expected_cols))
20289                > u64::from(self.config.spill.max_spill_cells)
20290        {
20291            self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
20292            return self.plan_spill_error_effects(
20293                vertex_id,
20294                "SpillTooLarge",
20295                expected_rows,
20296                expected_cols,
20297            );
20298        }
20299        if !matches!(&value, LiteralValue::Error(e) if e.kind == ExcelErrorKind::Spill) {
20300            self.blocked_pending_spills
20301                .retain(|entry| entry.0 != vertex_id);
20302            self.spill_blocker_waiters.forget(vertex_id);
20303        }
20304        let has_spill = self
20305            .graph
20306            .spill_cells_for_anchor(vertex_id)
20307            .is_some_and(|c| !c.is_empty());
20308
20309        let mut effects = Vec::new();
20310        if has_spill {
20311            effects.push(Effect::SpillClear {
20312                anchor_vertex: vertex_id,
20313            });
20314        }
20315        effects.push(Effect::WriteCell { vertex_id, value });
20316        Ok(effects)
20317    }
20318
20319    /// Plan effects for a formula vertex that produced an array result.
20320    fn plan_array_effects(
20321        &mut self,
20322        vertex_id: VertexId,
20323        rows: Vec<Vec<LiteralValue>>,
20324    ) -> Result<Vec<Effect>, ExcelError> {
20325        // Lightweight mutation needed for correct spill-blocking checks.
20326        self.graph.set_kind(vertex_id, VertexKind::FormulaArray);
20327
20328        let anchor = self
20329            .graph
20330            .get_cell_ref(vertex_id)
20331            .expect("cell ref for vertex");
20332        let sheet_id = anchor.sheet_id;
20333        let h = rows.len() as u32;
20334        let w = rows.first().map(|r| r.len()).unwrap_or(0) as u32;
20335
20336        // Hard cap to avoid vertex explosion from huge dynamic arrays.
20337        let spill_cells = (h as u64).saturating_mul(w as u64);
20338        if spill_cells > self.config.spill.max_spill_cells as u64 {
20339            return self.plan_spill_error_effects(vertex_id, "SpillTooLarge", h, w);
20340        }
20341
20342        // Bounds check to avoid out-of-range writes (align to AbsCoord capacity).
20343        const PACKED_MAX_ROW: u32 = 1_048_575;
20344        const PACKED_MAX_COL: u32 = 16_383;
20345        let end_row = anchor.coord.row().saturating_add(h).saturating_sub(1);
20346        let end_col = anchor.coord.col().saturating_add(w).saturating_sub(1);
20347        if end_row > PACKED_MAX_ROW || end_col > PACKED_MAX_COL {
20348            return self.plan_spill_error_effects(vertex_id, "Spill exceeds sheet bounds", h, w);
20349        }
20350
20351        if h != 0 && w != 0 && self.graph.table_intersects_spill(anchor, end_row, end_col) {
20352            return self.plan_spill_error_effects(vertex_id, "Spill blocked by table", h, w);
20353        }
20354        let mut targets = Vec::new();
20355        for r in 0..h {
20356            for c in 0..w {
20357                targets.push(self.graph.make_cell_ref_internal(
20358                    sheet_id,
20359                    anchor.coord.row() + r,
20360                    anchor.coord.col() + c,
20361                ));
20362            }
20363        }
20364
20365        if h != 0 && w != 0 {
20366            let occupied = self.pending_spill_occupied(anchor, end_row, end_col);
20367            if occupied
20368                || self
20369                    .blocked_pending_spills
20370                    .iter()
20371                    .any(|entry| entry.0 == vertex_id)
20372            {
20373                self.spill_mgr.release_owner(vertex_id);
20374                self.remember_pending_spill(
20375                    vertex_id,
20376                    anchor,
20377                    Region::rect(
20378                        sheet_id,
20379                        anchor.coord.row(),
20380                        end_row,
20381                        anchor.coord.col(),
20382                        end_col,
20383                    ),
20384                )?;
20385            }
20386            if occupied {
20387                return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
20388            }
20389        }
20390
20391        // Region lock via spill manager.
20392        match self.spill_mgr.reserve(
20393            vertex_id,
20394            anchor,
20395            SpillShape { rows: h, cols: w },
20396            SpillMeta {
20397                epoch: self.recalc_epoch,
20398                config: self.config.spill,
20399            },
20400        ) {
20401            Ok(()) => {
20402                // Validate spill region is available.
20403                let mut yield_effects = Vec::new();
20404                if let Err((e, blocker)) = self
20405                    .graph
20406                    .plan_spill_region_with_blocker(vertex_id, &targets)
20407                {
20408                    let yielding = if e.message.as_deref() == Some("BlockedBySpill") {
20409                        self.graph.spills_yielding_to(vertex_id, &targets)
20410                    } else {
20411                        None
20412                    };
20413                    if let Some(yielding) = yielding {
20414                        // A colliding spill later in (sheet, column, row)
20415                        // order yields: it is cleared and replans to
20416                        // `#SPILL!` with its dependents.
20417                        for &other in &yielding {
20418                            let (rows, cols) = self
20419                                .graph
20420                                .spill_extent_for_anchor(other)
20421                                .map(|(first, last)| {
20422                                    (
20423                                        last.coord.row() - first.coord.row() + 1,
20424                                        last.coord.col() - first.coord.col() + 1,
20425                                    )
20426                                })
20427                                .unwrap_or((0, 0));
20428                            yield_effects.push(Effect::SpillClear {
20429                                anchor_vertex: other,
20430                            });
20431                            yield_effects.push(Effect::WriteCell {
20432                                vertex_id: other,
20433                                value: Self::spill_error_value("Spill blocked", rows, cols),
20434                            });
20435                        }
20436                        self.graph.mark_dirty_many(&yielding);
20437                    } else {
20438                        // A formula or spill blocker can go away without this
20439                        // anchor recomputing: remember the attempted region so
20440                        // an edit inside it wakes the anchor.
20441                        if matches!(
20442                            e.message.as_deref(),
20443                            Some("BlockedByFormula" | "BlockedBySpill")
20444                        ) {
20445                            self.spill_blocker_waiters.wait(vertex_id, blocker);
20446                        }
20447                        return self.plan_spill_error_effects(vertex_id, "Spill blocked", h, w);
20448                    }
20449                }
20450
20451                // Arrow-canonical mode: graph planning cannot see non-empty value blockers because
20452                // cell values are not cached in the dependency graph. Consult Arrow storage to
20453                // detect occupied cells in the target region.
20454                if !self.graph.value_cache_enabled() {
20455                    let sheet_name = self.graph.sheet_name(sheet_id);
20456                    if let Some(asheet) = self.sheet_store().sheet(sheet_name) {
20457                        for cell in targets.iter() {
20458                            // Allow overwriting the anchor itself.
20459                            if *cell == anchor {
20460                                continue;
20461                            }
20462                            // Allow cells already owned by a spill (plan() validated spill ownership).
20463                            if self.graph.spill_registry_anchor_for_cell(*cell).is_some() {
20464                                continue;
20465                            }
20466                            // Skip formula blockers; plan() handled them (or allowed).
20467                            if let Some(vid) = self.graph.get_vertex_id_for_address(cell)
20468                                && vid != vertex_id
20469                            {
20470                                match self.graph.get_vertex_kind(vid) {
20471                                    VertexKind::FormulaScalar | VertexKind::FormulaArray => {
20472                                        continue;
20473                                    }
20474                                    _ => {}
20475                                }
20476                            }
20477
20478                            let v = asheet.get_cell_value(
20479                                cell.coord.row() as usize,
20480                                cell.coord.col() as usize,
20481                            );
20482                            if !matches!(v, LiteralValue::Empty) {
20483                                return self.plan_spill_error_effects(
20484                                    vertex_id,
20485                                    "BlockedByValue",
20486                                    h,
20487                                    w,
20488                                );
20489                            }
20490                        }
20491                    }
20492                }
20493
20494                let top_left = rows
20495                    .first()
20496                    .and_then(|r| r.first())
20497                    .cloned()
20498                    .unwrap_or(LiteralValue::Empty);
20499
20500                let mut effects = yield_effects;
20501                // Clear previous spill if any.
20502                let has_prev = self
20503                    .graph
20504                    .spill_cells_for_anchor(vertex_id)
20505                    .is_some_and(|c| !c.is_empty());
20506                if has_prev {
20507                    effects.push(Effect::SpillClear {
20508                        anchor_vertex: vertex_id,
20509                    });
20510                }
20511                effects.push(Effect::SpillCommit {
20512                    anchor_vertex: vertex_id,
20513                    anchor_cell: anchor,
20514                    target_cells: targets,
20515                    values: rows,
20516                });
20517                effects.push(Effect::WriteCell {
20518                    vertex_id,
20519                    value: top_left,
20520                });
20521                Ok(effects)
20522            }
20523            Err(e) => {
20524                let msg = e.message.unwrap_or_else(|| "Spill blocked".to_string());
20525                self.plan_spill_error_effects(vertex_id, &msg, h, w)
20526            }
20527        }
20528    }
20529
20530    /// The `#SPILL!` value a blocked anchor holds.
20531    fn spill_error_value(message: &str, expected_rows: u32, expected_cols: u32) -> LiteralValue {
20532        LiteralValue::Error(
20533            ExcelError::new(ExcelErrorKind::Spill)
20534                .with_message(message)
20535                .with_extra(formualizer_common::ExcelErrorExtra::Spill {
20536                    expected_rows,
20537                    expected_cols,
20538                }),
20539        )
20540    }
20541
20542    /// Build the effect list for a spill that failed validation.
20543    fn plan_spill_error_effects(
20544        &mut self,
20545        vertex_id: VertexId,
20546        message: &str,
20547        expected_rows: u32,
20548        expected_cols: u32,
20549    ) -> Result<Vec<Effect>, ExcelError> {
20550        self.spill_mgr.release_owner(vertex_id);
20551        let spill_val = Self::spill_error_value(message, expected_rows, expected_cols);
20552
20553        let effects = vec![
20554            Effect::SpillClear {
20555                anchor_vertex: vertex_id,
20556            },
20557            Effect::WriteCell {
20558                vertex_id,
20559                value: spill_val,
20560            },
20561        ];
20562        Ok(effects)
20563    }
20564
20565    /// Apply a single effect, performing the actual graph mutations.
20566    pub(crate) fn apply_effect(
20567        &mut self,
20568        effect: &Effect,
20569        delta: Option<&mut DeltaCollector>,
20570        log: Option<&mut ChangeLog>,
20571    ) -> Result<(), ExcelError> {
20572        self.apply_effect_with_computed_writes(effect, delta, log, None)
20573    }
20574
20575    fn apply_effect_with_computed_writes(
20576        &mut self,
20577        effect: &Effect,
20578        delta: Option<&mut DeltaCollector>,
20579        log: Option<&mut ChangeLog>,
20580        computed_writes: Option<&mut ComputedWriteBuffer>,
20581    ) -> Result<(), ExcelError> {
20582        match effect {
20583            Effect::WriteCell { vertex_id, value } => {
20584                self.apply_write_cell(*vertex_id, value, delta, computed_writes)?;
20585            }
20586            Effect::SpillClear { anchor_vertex } => {
20587                self.apply_spill_clear(*anchor_vertex, delta, log, computed_writes)?;
20588            }
20589            Effect::SpillCommit {
20590                anchor_vertex,
20591                anchor_cell: _,
20592                target_cells,
20593                values,
20594            } => {
20595                self.apply_spill_commit(
20596                    *anchor_vertex,
20597                    target_cells,
20598                    values.clone(),
20599                    delta,
20600                    log,
20601                    computed_writes,
20602                )?;
20603            }
20604        }
20605        Ok(())
20606    }
20607
20608    /// Apply a WriteCell effect.
20609    fn apply_write_cell(
20610        &mut self,
20611        vertex_id: VertexId,
20612        value: &LiteralValue,
20613        delta: Option<&mut DeltaCollector>,
20614        mut computed_writes: Option<&mut ComputedWriteBuffer>,
20615    ) -> Result<(), ExcelError> {
20616        if let Some(d) = delta
20617            && d.mode != DeltaMode::Off
20618        {
20619            if let Some(buffer) = computed_writes.as_deref_mut() {
20620                self.flush_computed_write_buffer(buffer)?;
20621            }
20622            if let Some(cell) = self.graph.get_cell_ref_for_vertex(vertex_id) {
20623                let sheet_name = self.graph.sheet_name(cell.sheet_id);
20624                let old = self
20625                    .read_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20626                    .unwrap_or(LiteralValue::Empty);
20627                if old != *value {
20628                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
20629                }
20630            }
20631        }
20632        self.graph.update_vertex_value_ref(vertex_id, value);
20633        self.record_vertex_value_to_overlay(vertex_id, value, computed_writes)?;
20634        Ok(())
20635    }
20636
20637    /// Apply a SpillClear effect.
20638    fn apply_spill_clear(
20639        &mut self,
20640        anchor_vertex: VertexId,
20641        delta: Option<&mut DeltaCollector>,
20642        log: Option<&mut ChangeLog>,
20643        computed_writes: Option<&mut ComputedWriteBuffer>,
20644    ) -> Result<(), ExcelError> {
20645        if let Some(buffer) = computed_writes {
20646            self.flush_computed_write_buffer(buffer)?;
20647        }
20648
20649        let spill_cells = self.graph.spill_cells_to_clear(anchor_vertex);
20650        if spill_cells.is_empty() {
20651            return Ok(());
20652        }
20653
20654        // Snapshot for ChangeLog before clearing.
20655        let snapshot = if log.is_some() {
20656            self.snapshot_spill_for_anchor(anchor_vertex)
20657        } else {
20658            None
20659        };
20660
20661        // Record delta for cleared cells.
20662        if let Some(d) = delta
20663            && d.mode != DeltaMode::Off
20664        {
20665            let empty = LiteralValue::Empty;
20666            for cell in spill_cells.iter() {
20667                let sheet_name = self.graph.sheet_name(cell.sheet_id);
20668                let old = self
20669                    .get_cell_value(sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20670                    .unwrap_or(LiteralValue::Empty);
20671                if old != empty {
20672                    d.record_cell(cell.sheet_id, cell.coord.row(), cell.coord.col());
20673                }
20674            }
20675        }
20676
20677        self.graph.clear_spill_region(anchor_vertex);
20678        if let Some(scope) = Self::structural_scope_from_cells(&spill_cells) {
20679            self.record_structural_change(scope);
20680        }
20681
20682        // Mirror Empty to Arrow overlay for cleared cells.
20683        if self.config.arrow_storage_enabled
20684            && self.config.delta_overlay_enabled
20685            && self.config.write_formula_overlay_enabled
20686        {
20687            let empty = LiteralValue::Empty;
20688            for cell in spill_cells.iter() {
20689                let sheet_name = self.graph.sheet_name(cell.sheet_id).to_string();
20690                self.mirror_value_to_computed_overlay(
20691                    &sheet_name,
20692                    cell.coord.row() + 1,
20693                    cell.coord.col() + 1,
20694                    &empty,
20695                );
20696            }
20697        }
20698
20699        // ChangeLog.
20700        if let Some(log) = log
20701            && let Some(old) = snapshot
20702        {
20703            log.record(ChangeEvent::SpillCleared {
20704                anchor: anchor_vertex,
20705                old,
20706            });
20707        }
20708        Ok(())
20709    }
20710
20711    /// Apply a SpillCommit effect.
20712    fn apply_spill_commit(
20713        &mut self,
20714        anchor_vertex: VertexId,
20715        target_cells: &[CellRef],
20716        values: Vec<Vec<LiteralValue>>,
20717        delta: Option<&mut DeltaCollector>,
20718        log: Option<&mut ChangeLog>,
20719        computed_writes: Option<&mut ComputedWriteBuffer>,
20720    ) -> Result<(), ExcelError> {
20721        self.guard_pending_spill_commit(anchor_vertex, target_cells)?;
20722        if let Some(buffer) = computed_writes {
20723            self.flush_computed_write_buffer(buffer)?;
20724        }
20725
20726        // Snapshot for ChangeLog before commit.
20727        let old_snapshot = if log.is_some() {
20728            self.snapshot_spill_for_anchor(anchor_vertex)
20729        } else {
20730            None
20731        };
20732
20733        // Delegate to existing commit_spill_and_mirror for delta + overlay logic.
20734        self.commit_spill_and_mirror(anchor_vertex, target_cells, values.clone(), delta)?;
20735
20736        // ChangeLog.
20737        if let Some(log) = log {
20738            log.record(ChangeEvent::SpillCommitted {
20739                anchor: anchor_vertex,
20740                old: old_snapshot,
20741                new: SpillSnapshot {
20742                    target_cells: target_cells.to_vec(),
20743                    values,
20744                },
20745            });
20746        }
20747        Ok(())
20748    }
20749
20750    /// Snapshot a spill region for ChangeLog recording.
20751    ///
20752    /// Extracted from `VertexEditor::snapshot_spill_for_anchor` to be usable
20753    /// without creating a `VertexEditor`.
20754    fn snapshot_spill_for_anchor(&self, anchor: VertexId) -> Option<SpillSnapshot> {
20755        let cells = self.graph.spill_cells_for_anchor(anchor)?.to_vec();
20756        if cells.is_empty() {
20757            return None;
20758        }
20759
20760        let max = self.config.spill.max_spill_cells as usize;
20761        let mut cells = cells;
20762        if cells.len() > max {
20763            cells.truncate(max);
20764        }
20765
20766        let first = *cells.first().expect("non-empty spill cells");
20767        let sheet_name = self.graph.sheet_name(first.sheet_id).to_string();
20768        let row0 = first.coord.row();
20769        let col0 = first.coord.col();
20770
20771        let mut max_row = row0;
20772        let mut max_col = col0;
20773        let mut by_coord: FxHashMap<(u32, u32), LiteralValue> = FxHashMap::default();
20774        for cell in &cells {
20775            max_row = max_row.max(cell.coord.row());
20776            max_col = max_col.max(cell.coord.col());
20777            let v = self
20778                .get_cell_value(&sheet_name, cell.coord.row() + 1, cell.coord.col() + 1)
20779                .unwrap_or(LiteralValue::Empty);
20780            by_coord.insert((cell.coord.row(), cell.coord.col()), v);
20781        }
20782
20783        let rows = (max_row - row0 + 1) as usize;
20784        let cols = (max_col - col0 + 1) as usize;
20785        let mut values: Vec<Vec<LiteralValue>> = Vec::with_capacity(rows);
20786        for r in 0..rows {
20787            let mut row: Vec<LiteralValue> = Vec::with_capacity(cols);
20788            for c in 0..cols {
20789                row.push(
20790                    by_coord
20791                        .get(&(row0 + r as u32, col0 + c as u32))
20792                        .cloned()
20793                        .unwrap_or(LiteralValue::Empty),
20794                );
20795            }
20796            values.push(row);
20797        }
20798
20799        Some(SpillSnapshot {
20800            target_cells: cells,
20801            values,
20802        })
20803    }
20804
20805    fn flush_before_range_dependent_vertex(
20806        &mut self,
20807        vertex_id: VertexId,
20808        computed_writes: &mut ComputedWriteBuffer,
20809    ) -> Result<(), ExcelError> {
20810        if self.graph.reads_compressed_range(vertex_id) {
20811            self.flush_computed_write_buffer(computed_writes)?;
20812        }
20813        Ok(())
20814    }
20815
20816    fn plan_vertex_effects_with_computed_flush(
20817        &mut self,
20818        vertex_id: VertexId,
20819        computed_value: LiteralValue,
20820        computed_writes: &mut ComputedWriteBuffer,
20821    ) -> Result<Vec<Effect>, ExcelError> {
20822        if matches!(&computed_value, LiteralValue::Array(_)) {
20823            self.flush_computed_write_buffer(computed_writes)?;
20824        }
20825        self.plan_vertex_effects(vertex_id, computed_value)
20826    }
20827
20828    // ── Layer evaluation via effects pipeline ──────────────────────────────
20829
20830    fn evaluate_small_layer_direct_effects(
20831        &mut self,
20832        layer: &super::scheduler::Layer,
20833        delta: Option<&mut DeltaCollector>,
20834        log: Option<&mut ChangeLog>,
20835        cancel_flag: Option<&AtomicBool>,
20836        cancel_check_every: usize,
20837        cancel_message: &'static str,
20838    ) -> Result<usize, ExcelError> {
20839        let cancel = cancel_flag.map(|flag| (flag, cancel_check_every, cancel_message));
20840        self.evaluate_layer_units(layer, delta, log, cancel, false)
20841    }
20842
20843    /// Sequential layer walk over its units (single cells and family runs):
20844    /// each unit evaluates, then its vertices' effects apply in order. With
20845    /// `buffered`, computed writes coalesce in a layer buffer (flushed before
20846    /// a unit that reads a compressed range, before array results, and at
20847    /// the end); otherwise they apply directly. `cancel` = (flag, check every
20848    /// N vertices, message).
20849    fn evaluate_layer_units(
20850        &mut self,
20851        layer: &super::scheduler::Layer,
20852        delta: Option<&mut DeltaCollector>,
20853        log: Option<&mut ChangeLog>,
20854        cancel: Option<(&AtomicBool, usize, &'static str)>,
20855        buffered: bool,
20856    ) -> Result<usize, ExcelError> {
20857        self.evaluate_layer_units_until(layer, delta, log, cancel, buffered, None)
20858    }
20859
20860    /// [`Self::evaluate_layer_units`] that stops before the next unit once
20861    /// `stop_at` has passed; returns the vertices evaluated (a prefix of
20862    /// the layer, all committed).
20863    fn evaluate_layer_units_until(
20864        &mut self,
20865        layer: &super::scheduler::Layer,
20866        mut delta: Option<&mut DeltaCollector>,
20867        mut log: Option<&mut ChangeLog>,
20868        cancel: Option<(&AtomicBool, usize, &'static str)>,
20869        buffered: bool,
20870        stop_at: Option<crate::instant::FzInstant>,
20871    ) -> Result<usize, ExcelError> {
20872        // A chain unit: its run through the chain lift, or else cell by
20873        // cell in row order, each written before the next reads it.
20874        let mut chain_values = None;
20875        if layer.sequential && !layer.runs.is_empty() {
20876            chain_values = match layer.runs.as_slice() {
20877                [run] if run.start == 0 && run.len as usize == layer.vertices.len() => {
20878                    self.try_chain_lift(*run, &layer.vertices)
20879                }
20880                _ => None,
20881            };
20882            if chain_values.is_none() {
20883                let cells = super::scheduler::Layer {
20884                    vertices: layer.vertices.clone(),
20885                    runs: Vec::new(),
20886                    sequential: true,
20887                };
20888                return self.evaluate_layer_units_until(&cells, delta, log, cancel, false, stop_at);
20889            }
20890        }
20891        let chained = chain_values.is_some();
20892        // The chain lift computed every member: one block write, as a run.
20893        let buffered = buffered || chained;
20894        // A dynamic reader's targets are not always ordered before it (its
20895        // pre-probe or observed reads can miss them, e.g. after a structural
20896        // edit). In a buffered layer a member's dirty flag is cleared at its
20897        // commit but its value written at the flush: the members committed
20898        // since the last flush count as dirty for the reader's freshness
20899        // check, which then re-plans it after them.
20900        let track_unflushed = buffered
20901            && self.freshness_armed()
20902            && layer.vertices.iter().any(|&v| self.graph.is_dynamic(v));
20903        let mut committed_unit: &[VertexId] = &[];
20904        let mut computed_writes = ComputedWriteBuffer::default();
20905        let mut next_check = 0usize;
20906        let mut done = 0usize;
20907        for unit in layer_units(layer) {
20908            if done > 0
20909                && let Some(stop_at) = stop_at
20910                && crate::instant::FzInstant::now() >= stop_at
20911            {
20912                self.flush_computed_write_buffer(&mut computed_writes)?;
20913                if track_unflushed {
20914                    self.freshness_flushed();
20915                }
20916                return Ok(done);
20917            }
20918            if let Some((flag, every, message)) = cancel
20919                && every > 0
20920                && done >= next_check
20921            {
20922                next_check = (done / every + 1) * every;
20923                if flag.load(Ordering::Relaxed) {
20924                    if buffered {
20925                        self.flush_computed_write_buffer(&mut computed_writes)?;
20926                    }
20927                    return Err(ExcelError::new(ExcelErrorKind::Cancelled)
20928                        .with_message(message.to_string()));
20929                }
20930            }
20931            if buffered && self.unit_reads_compressed_range(layer, unit) {
20932                self.flush_computed_write_buffer(&mut computed_writes)?;
20933            }
20934            // The previous unit's members are committed (dirty flags
20935            // cleared); while their values wait in the buffer, a dynamic
20936            // reader's read of them is stale (`freshness_dirty_reads`).
20937            if track_unflushed {
20938                if computed_writes.is_empty() {
20939                    self.freshness_flushed();
20940                } else {
20941                    self.freshness_note_unflushed(committed_unit);
20942                }
20943                committed_unit = unit_members(layer, unit);
20944            }
20945            let values = match (chain_values.take(), unit) {
20946                (Some(chain), LayerUnit::Run(run)) => {
20947                    if let Err(e) = self.live_cancellation_after_work(UNIT_CANCELLED) {
20948                        self.flush_computed_write_buffer(&mut computed_writes)?;
20949                        return Err(e);
20950                    }
20951                    let members =
20952                        &layer.vertices[run.start as usize..(run.start + run.len) as usize];
20953                    let delta_active = delta.as_deref().is_some_and(|d| d.mode != DeltaMode::Off);
20954                    match self.commit_run_numbers(
20955                        run,
20956                        members,
20957                        &chain,
20958                        delta_active,
20959                        Some(&mut computed_writes),
20960                    ) {
20961                        Ok(true) => {
20962                            done += chain.len();
20963                            continue;
20964                        }
20965                        Ok(false) => {}
20966                        Err(e) => {
20967                            self.flush_computed_write_buffer(&mut computed_writes)?;
20968                            return Err(e);
20969                        }
20970                    }
20971                    members
20972                        .iter()
20973                        .copied()
20974                        .zip(chain.into_iter().map(LiteralValue::Number))
20975                        .collect()
20976                }
20977                (_, unit) => self.evaluate_unit_immutable(layer, unit),
20978            };
20979            // Post-work boundary: the unit ran while (or after) the request
20980            // was cancelled; it is not committed and stays dirty.
20981            if let Err(e) = self.live_cancellation_after_work(UNIT_CANCELLED) {
20982                self.flush_computed_write_buffer(&mut computed_writes)?;
20983                return Err(e);
20984            }
20985            done += values.len();
20986            if let LayerUnit::Run(run) = unit {
20987                let members = &layer.vertices[run.start as usize..(run.start + run.len) as usize];
20988                let delta_active = delta.as_deref().is_some_and(|d| d.mode != DeltaMode::Off);
20989                let committed = self.commit_run_scalars(
20990                    run,
20991                    members,
20992                    &values,
20993                    delta_active,
20994                    buffered.then_some(&mut computed_writes),
20995                );
20996                match committed {
20997                    Ok(true) => continue,
20998                    Ok(false) => {}
20999                    Err(e) => {
21000                        self.flush_computed_write_buffer(&mut computed_writes)?;
21001                        return Err(e);
21002                    }
21003                }
21004            }
21005            // A run unit's members were all evaluated before any commits
21006            // (FR5, FORM-192); a single cell is committed as it runs.
21007            let guarded = self.freshness_begin_batch_commit(&values);
21008            for (vertex_id, value) in values {
21009                let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
21010                let effects = if buffered {
21011                    self.plan_vertex_effects_with_computed_flush(
21012                        vertex_id,
21013                        value,
21014                        &mut computed_writes,
21015                    )
21016                } else {
21017                    self.plan_vertex_effects(vertex_id, value)
21018                };
21019                let effects = match effects {
21020                    Ok(effects) => effects,
21021                    Err(e) => {
21022                        self.flush_computed_write_buffer(&mut computed_writes)?;
21023                        return Err(e);
21024                    }
21025                };
21026                for effect in &effects {
21027                    if let Err(e) = self.apply_effect_with_computed_writes(
21028                        effect,
21029                        delta.as_deref_mut(),
21030                        log.as_deref_mut(),
21031                        buffered.then_some(&mut computed_writes),
21032                    ) {
21033                        self.flush_computed_write_buffer(&mut computed_writes)?;
21034                        return Err(e);
21035                    }
21036                }
21037                self.freshness_keep_redirtied(redirtied, vertex_id);
21038            }
21039        }
21040        self.flush_computed_write_buffer(&mut computed_writes)?;
21041        if track_unflushed {
21042            self.freshness_flushed();
21043        }
21044        // Debug builds: every chain member equals the per-cell path, now
21045        // that the members above it are written.
21046        #[cfg(debug_assertions)]
21047        if chained {
21048            for &v in &layer.vertices {
21049                let cell = self.graph.get_cell_ref(v);
21050                let (sheet, row, col) = cell
21051                    .map(|c| {
21052                        (
21053                            self.graph.sheet_name(c.sheet_id).to_string(),
21054                            c.coord.row() + 1,
21055                            c.coord.col() + 1,
21056                        )
21057                    })
21058                    .expect("chain member cell");
21059                let written = self.get_cell_value(&sheet, row, col);
21060                let oracle = self
21061                    .evaluate_vertex_immutable(v)
21062                    .unwrap_or_else(LiteralValue::Error);
21063                assert!(
21064                    written
21065                        .as_ref()
21066                        .is_some_and(|w| same_value_bits(w, &oracle)),
21067                    "chain member {sheet}!R{row}C{col}: {written:?} vs per-cell {oracle:?}"
21068                );
21069            }
21070        }
21071        #[cfg(not(debug_assertions))]
21072        let _ = chained;
21073        Ok(layer.vertices.len())
21074    }
21075
21076    /// Evaluate a layer sequentially using the effects pipeline.
21077    fn evaluate_layer_sequential_effects(
21078        &mut self,
21079        layer: &super::scheduler::Layer,
21080    ) -> Result<usize, ExcelError> {
21081        let buffered = buffer_layer_writes(layer);
21082        self.evaluate_layer_units(layer, None, None, None, buffered)
21083    }
21084
21085    /// Evaluate a layer sequentially with delta collection via effects pipeline.
21086    fn evaluate_layer_sequential_with_delta_effects(
21087        &mut self,
21088        layer: &super::scheduler::Layer,
21089        delta: &mut DeltaCollector,
21090    ) -> Result<usize, ExcelError> {
21091        let buffered = buffer_layer_writes(layer);
21092        self.evaluate_layer_units(layer, Some(delta), None, None, buffered)
21093    }
21094
21095    /// Evaluate a layer sequentially with cancellation support via effects pipeline.
21096    fn evaluate_layer_sequential_cancellable_effects(
21097        &mut self,
21098        layer: &super::scheduler::Layer,
21099        cancel_flag: &AtomicBool,
21100    ) -> Result<usize, ExcelError> {
21101        let buffered = buffer_layer_writes(layer);
21102        let cancel = (cancel_flag, 256, "Evaluation cancelled within layer");
21103        self.evaluate_layer_units(layer, None, None, Some(cancel), buffered)
21104    }
21105
21106    /// Evaluate a layer sequentially with more frequent cancellation for demand-driven eval.
21107    fn evaluate_layer_sequential_cancellable_demand_driven_effects(
21108        &mut self,
21109        layer: &super::scheduler::Layer,
21110        cancel_flag: &AtomicBool,
21111    ) -> Result<usize, ExcelError> {
21112        let buffered = buffer_layer_writes(layer);
21113        let cancel = (
21114            cancel_flag,
21115            128,
21116            "Demand-driven evaluation cancelled within layer",
21117        );
21118        self.evaluate_layer_units(layer, None, None, Some(cancel), buffered)
21119    }
21120
21121    /// Evaluate a layer in parallel, applying via effects pipeline.
21122    fn evaluate_layer_parallel_effects(
21123        &mut self,
21124        layer: &super::scheduler::Layer,
21125        min_chunk: u32,
21126    ) -> Result<usize, ExcelError> {
21127        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
21128
21129        let phases = self.parallel_phases(layer);
21130        let mut applied = 0usize;
21131
21132        for (units, group) in &phases {
21133            let group = &group[..];
21134            if group.is_empty() {
21135                continue;
21136            }
21137            let mut computed_writes = ComputedWriteBuffer::default();
21138
21139            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
21140                thread_pool.install(|| self.evaluate_units_parallel(layer, units, None, min_chunk));
21141
21142            // Post-work boundary: a group evaluated across a live
21143            // cancellation is not committed (it stays dirty).
21144            let results = results.and_then(|results| {
21145                self.live_cancellation_after_work(GROUP_CANCELLED)
21146                    .map(|()| results)
21147            });
21148            // FR3: a parallel group is one commit unit; one stale reader
21149            // drops the whole group (it stays dirty and replans).
21150            self.freshness_gate_group(group);
21151            match results {
21152                Ok(vertex_results) => {
21153                    let (vertex_results, committed) = self.commit_parallel_runs(
21154                        layer,
21155                        units,
21156                        vertex_results,
21157                        false,
21158                        &mut computed_writes,
21159                    )?;
21160                    applied = applied.saturating_add(committed);
21161                    // Arrays first, then scalars — establishes spill regions before
21162                    // scalar results that might land inside a spilled region.
21163                    // FR5 (FORM-192): members evaluated together must not
21164                    // clear a re-dirty from a spill committed before them.
21165                    let guarded = self.freshness_begin_batch_commit(&vertex_results);
21166                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
21167                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
21168                    for (vertex_id, result) in vertex_results {
21169                        if matches!(result, LiteralValue::Array(_)) {
21170                            arrays.push((vertex_id, result));
21171                        } else {
21172                            others.push((vertex_id, result));
21173                        }
21174                    }
21175                    for (vertex_id, result) in arrays {
21176                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
21177                        let effects = match self.plan_vertex_effects_with_computed_flush(
21178                            vertex_id,
21179                            result,
21180                            &mut computed_writes,
21181                        ) {
21182                            Ok(effects) => effects,
21183                            Err(e) => {
21184                                self.flush_computed_write_buffer(&mut computed_writes)?;
21185                                return Err(e);
21186                            }
21187                        };
21188                        for effect in &effects {
21189                            if let Err(e) = self.apply_effect_with_computed_writes(
21190                                effect,
21191                                None,
21192                                None,
21193                                Some(&mut computed_writes),
21194                            ) {
21195                                self.flush_computed_write_buffer(&mut computed_writes)?;
21196                                return Err(e);
21197                            }
21198                        }
21199                        self.freshness_keep_redirtied(redirtied, vertex_id);
21200                        applied = applied.saturating_add(1);
21201                    }
21202                    // Make all array spill/top-left writes visible before scalar effects in this group.
21203                    self.flush_computed_write_buffer(&mut computed_writes)?;
21204                    for (vertex_id, result) in others {
21205                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
21206                        let effects = match self.plan_vertex_effects_with_computed_flush(
21207                            vertex_id,
21208                            result,
21209                            &mut computed_writes,
21210                        ) {
21211                            Ok(effects) => effects,
21212                            Err(e) => {
21213                                self.flush_computed_write_buffer(&mut computed_writes)?;
21214                                return Err(e);
21215                            }
21216                        };
21217                        for effect in &effects {
21218                            if let Err(e) = self.apply_effect_with_computed_writes(
21219                                effect,
21220                                None,
21221                                None,
21222                                Some(&mut computed_writes),
21223                            ) {
21224                                self.flush_computed_write_buffer(&mut computed_writes)?;
21225                                return Err(e);
21226                            }
21227                        }
21228                        self.freshness_keep_redirtied(redirtied, vertex_id);
21229                        applied = applied.saturating_add(1);
21230                    }
21231                    // Flush at the group boundary; phase1 must be visible before phase2.
21232                    self.flush_computed_write_buffer(&mut computed_writes)?;
21233                }
21234                Err(e) => {
21235                    self.flush_computed_write_buffer(&mut computed_writes)?;
21236                    return Err(e);
21237                }
21238            }
21239        }
21240
21241        Ok(applied)
21242    }
21243
21244    /// Evaluate a layer in parallel with delta collection via effects pipeline.
21245    fn evaluate_layer_parallel_with_delta_effects(
21246        &mut self,
21247        layer: &super::scheduler::Layer,
21248        delta: &mut DeltaCollector,
21249    ) -> Result<usize, ExcelError> {
21250        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
21251
21252        let phases = self.parallel_phases(layer);
21253        let mut applied = 0usize;
21254
21255        for (units, group) in &phases {
21256            let group = &group[..];
21257            if group.is_empty() {
21258                continue;
21259            }
21260            let mut computed_writes = ComputedWriteBuffer::default();
21261            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> =
21262                thread_pool.install(|| self.evaluate_units_parallel(layer, units, None, 8));
21263
21264            // Post-work boundary: a group evaluated across a live
21265            // cancellation is not committed (it stays dirty).
21266            let results = results.and_then(|results| {
21267                self.live_cancellation_after_work(GROUP_CANCELLED)
21268                    .map(|()| results)
21269            });
21270            // FR3: a parallel group is one commit unit; one stale reader
21271            // drops the whole group (it stays dirty and replans).
21272            self.freshness_gate_group(group);
21273            match results {
21274                Ok(vertex_results) => {
21275                    let (vertex_results, committed) = self.commit_parallel_runs(
21276                        layer,
21277                        units,
21278                        vertex_results,
21279                        delta.mode != DeltaMode::Off,
21280                        &mut computed_writes,
21281                    )?;
21282                    applied = applied.saturating_add(committed);
21283                    // FR5 (FORM-192): members evaluated together must not
21284                    // clear a re-dirty from a spill committed before them.
21285                    let guarded = self.freshness_begin_batch_commit(&vertex_results);
21286                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
21287                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
21288                    for (vertex_id, result) in vertex_results {
21289                        if matches!(result, LiteralValue::Array(_)) {
21290                            arrays.push((vertex_id, result));
21291                        } else {
21292                            others.push((vertex_id, result));
21293                        }
21294                    }
21295                    for (vertex_id, result) in arrays {
21296                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
21297                        let effects = match self.plan_vertex_effects_with_computed_flush(
21298                            vertex_id,
21299                            result,
21300                            &mut computed_writes,
21301                        ) {
21302                            Ok(effects) => effects,
21303                            Err(e) => {
21304                                self.flush_computed_write_buffer(&mut computed_writes)?;
21305                                return Err(e);
21306                            }
21307                        };
21308                        for effect in &effects {
21309                            if let Err(e) = self.apply_effect_with_computed_writes(
21310                                effect,
21311                                Some(delta),
21312                                None,
21313                                Some(&mut computed_writes),
21314                            ) {
21315                                self.flush_computed_write_buffer(&mut computed_writes)?;
21316                                return Err(e);
21317                            }
21318                        }
21319                        self.freshness_keep_redirtied(redirtied, vertex_id);
21320                        applied = applied.saturating_add(1);
21321                    }
21322                    self.flush_computed_write_buffer(&mut computed_writes)?;
21323                    for (vertex_id, result) in others {
21324                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
21325                        let effects = match self.plan_vertex_effects_with_computed_flush(
21326                            vertex_id,
21327                            result,
21328                            &mut computed_writes,
21329                        ) {
21330                            Ok(effects) => effects,
21331                            Err(e) => {
21332                                self.flush_computed_write_buffer(&mut computed_writes)?;
21333                                return Err(e);
21334                            }
21335                        };
21336                        for effect in &effects {
21337                            if let Err(e) = self.apply_effect_with_computed_writes(
21338                                effect,
21339                                Some(delta),
21340                                None,
21341                                Some(&mut computed_writes),
21342                            ) {
21343                                self.flush_computed_write_buffer(&mut computed_writes)?;
21344                                return Err(e);
21345                            }
21346                        }
21347                        self.freshness_keep_redirtied(redirtied, vertex_id);
21348                        applied = applied.saturating_add(1);
21349                    }
21350                    self.flush_computed_write_buffer(&mut computed_writes)?;
21351                }
21352                Err(e) => {
21353                    self.flush_computed_write_buffer(&mut computed_writes)?;
21354                    return Err(e);
21355                }
21356            }
21357        }
21358
21359        Ok(applied)
21360    }
21361
21362    /// Evaluate a layer in parallel with cancellation support via effects pipeline.
21363    fn evaluate_layer_parallel_cancellable_effects(
21364        &mut self,
21365        layer: &super::scheduler::Layer,
21366        cancel_flag: &AtomicBool,
21367    ) -> Result<usize, ExcelError> {
21368        let thread_pool = self.thread_pool.as_ref().unwrap().clone();
21369
21370        if cancel_flag.load(Ordering::Relaxed) {
21371            return Err(ExcelError::new(ExcelErrorKind::Cancelled)
21372                .with_message("Parallel evaluation cancelled before starting".to_string()));
21373        }
21374
21375        let phases = self.parallel_phases(layer);
21376        let mut applied = 0usize;
21377
21378        for (units, group) in &phases {
21379            let group = &group[..];
21380            if group.is_empty() {
21381                continue;
21382            }
21383            let mut computed_writes = ComputedWriteBuffer::default();
21384
21385            let results: Result<Vec<(VertexId, LiteralValue)>, ExcelError> = thread_pool
21386                .install(|| self.evaluate_units_parallel(layer, units, Some(cancel_flag), 8));
21387
21388            // Post-work boundary: a group evaluated across a live
21389            // cancellation is not committed (it stays dirty).
21390            let results = results.and_then(|results| {
21391                self.live_cancellation_after_work(GROUP_CANCELLED)
21392                    .map(|()| results)
21393            });
21394            // FR3: a parallel group is one commit unit; one stale reader
21395            // drops the whole group (it stays dirty and replans).
21396            self.freshness_gate_group(group);
21397            match results {
21398                Ok(vertex_results) => {
21399                    let (vertex_results, committed) = self.commit_parallel_runs(
21400                        layer,
21401                        units,
21402                        vertex_results,
21403                        false,
21404                        &mut computed_writes,
21405                    )?;
21406                    applied = applied.saturating_add(committed);
21407                    // FR5 (FORM-192): members evaluated together must not
21408                    // clear a re-dirty from a spill committed before them.
21409                    let guarded = self.freshness_begin_batch_commit(&vertex_results);
21410                    let mut arrays: Vec<(VertexId, LiteralValue)> = Vec::new();
21411                    let mut others: Vec<(VertexId, LiteralValue)> = Vec::new();
21412                    for (vertex_id, result) in vertex_results {
21413                        if matches!(result, LiteralValue::Array(_)) {
21414                            arrays.push((vertex_id, result));
21415                        } else {
21416                            others.push((vertex_id, result));
21417                        }
21418                    }
21419                    for (vertex_id, result) in arrays {
21420                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
21421                        let effects = match self.plan_vertex_effects_with_computed_flush(
21422                            vertex_id,
21423                            result,
21424                            &mut computed_writes,
21425                        ) {
21426                            Ok(effects) => effects,
21427                            Err(e) => {
21428                                self.flush_computed_write_buffer(&mut computed_writes)?;
21429                                return Err(e);
21430                            }
21431                        };
21432                        for effect in &effects {
21433                            if let Err(e) = self.apply_effect_with_computed_writes(
21434                                effect,
21435                                None,
21436                                None,
21437                                Some(&mut computed_writes),
21438                            ) {
21439                                self.flush_computed_write_buffer(&mut computed_writes)?;
21440                                return Err(e);
21441                            }
21442                        }
21443                        self.freshness_keep_redirtied(redirtied, vertex_id);
21444                        applied = applied.saturating_add(1);
21445                    }
21446                    self.flush_computed_write_buffer(&mut computed_writes)?;
21447                    for (vertex_id, result) in others {
21448                        let redirtied = self.freshness_batch_redirtied(guarded, vertex_id);
21449                        let effects = match self.plan_vertex_effects_with_computed_flush(
21450                            vertex_id,
21451                            result,
21452                            &mut computed_writes,
21453                        ) {
21454                            Ok(effects) => effects,
21455                            Err(e) => {
21456                                self.flush_computed_write_buffer(&mut computed_writes)?;
21457                                return Err(e);
21458                            }
21459                        };
21460                        for effect in &effects {
21461                            if let Err(e) = self.apply_effect_with_computed_writes(
21462                                effect,
21463                                None,
21464                                None,
21465                                Some(&mut computed_writes),
21466                            ) {
21467                                self.flush_computed_write_buffer(&mut computed_writes)?;
21468                                return Err(e);
21469                            }
21470                        }
21471                        self.freshness_keep_redirtied(redirtied, vertex_id);
21472                        applied = applied.saturating_add(1);
21473                    }
21474                    self.flush_computed_write_buffer(&mut computed_writes)?;
21475                }
21476                Err(e) => {
21477                    self.flush_computed_write_buffer(&mut computed_writes)?;
21478                    return Err(e);
21479                }
21480            }
21481        }
21482
21483        Ok(applied)
21484    }
21485
21486    // ── Top-level evaluate_all_logged ───────────────────────────────────────
21487
21488    /// Evaluate all dirty/volatile vertices, recording effects into a ChangeLog.
21489    ///
21490    /// This is the same flow as `evaluate_all` but threads a ChangeLog through
21491    /// every effect application so that spill commits/clears are captured.
21492    pub fn evaluate_all_logged(&mut self, log: &mut ChangeLog) -> Result<EvalResult, ExcelError> {
21493        self.observe_evaluation_resource_request(EvaluationRequestKind::FullLogged, |engine| {
21494            engine.evaluate_all_logged_unobserved(log)
21495        })
21496    }
21497
21498    fn evaluate_all_logged_unobserved(
21499        &mut self,
21500        log: &mut ChangeLog,
21501    ) -> Result<EvalResult, ExcelError> {
21502        self.observe_function_semantic_epoch()?;
21503        let _source_cache = self.source_cache_session();
21504        self.validate_deterministic_mode()?;
21505        if self.config.defer_graph_building {
21506            self.build_graph_all()?;
21507        }
21508        self.require_unified_authority()?;
21509        self.begin_evaluation_request();
21510        self.reset_virtual_dep_telemetry_if_disabled();
21511        let start = crate::instant::FzInstant::now();
21512        let mut computed_vertices = 0;
21513        let mut cycle_errors = 0;
21514
21515        let mut replan_iterations = 0;
21516        const MAX_REPLAN: usize = 5;
21517        let mut telemetry = self
21518            .config
21519            .enable_virtual_dep_telemetry
21520            .then(|| self.start_virtual_dep_telemetry());
21521
21522        log.begin_compound(format!("evaluate_all(epoch={})", self.recalc_epoch));
21523
21524        let result = (|| -> Result<EvalResult, ExcelError> {
21525            loop {
21526                let to_evaluate = self.graph.get_evaluation_vertices();
21527                if to_evaluate.is_empty() {
21528                    if let Some(t) = telemetry.as_mut()
21529                        && t.bailout_reason.is_none()
21530                    {
21531                        t.bailout_reason = Some("no_work");
21532                    }
21533                    break;
21534                }
21535
21536                let (schedule, old_vdeps, meta) = self.create_evaluation_schedule(&to_evaluate)?;
21537                if let Some(t) = telemetry.as_mut() {
21538                    Self::accumulate_schedule_meta(t, &meta);
21539                }
21540
21541                // Walk units in condensation order: stamp cycles at their
21542                // position, evaluate layers with ChangeLog recording.
21543                self.begin_pass(&schedule);
21544                for (unit_index, &unit) in schedule.units.iter().enumerate() {
21545                    match unit {
21546                        ScheduleUnit::Cycle(i) => {
21547                            // Journal integration (design doc §4 last row): the
21548                            // ChangeLog in this path only records SpillClear /
21549                            // SpillCommit events; WriteCell effects are never
21550                            // logged (see `apply_write_cell`). Runtime SCC tasks
21551                            // write values directly and never spill (§7.9 stamps
21552                            // would-be anchors), and their spill *teardown* is the
21553                            // same unlogged `stamp_cycle_error` the Static path
21554                            // already uses here — so direct commits coexist with
21555                            // the journal cleanly, with identical semantics to
21556                            // Static. Pinned by `scc_runtime_cycles` tests.
21557                            if self.handle_cycle_unit(schedule.unit_cycle(i), None, None, None)? > 0
21558                            {
21559                                cycle_errors += 1;
21560                            }
21561                        }
21562                        ScheduleUnit::Layer(i) => {
21563                            computed_vertices +=
21564                                self.evaluate_layer_logged(schedule.unit_layer(i), log)?;
21565                        }
21566                    }
21567                    if self.stop_after_unit(&schedule, unit_index) {
21568                        break;
21569                    }
21570                }
21571
21572                let changed_vertices = self.changed_virtual_dep_vertices(&to_evaluate, &old_vdeps);
21573                if let Some(t) = telemetry.as_mut() {
21574                    t.changed_vdeps_total += changed_vertices.len();
21575                }
21576                self.resource_checkpoint(0)?;
21577                if !self.finish_pass_dirty(&to_evaluate, &changed_vertices) {
21578                    if let Some(t) = telemetry.as_mut() {
21579                        t.bailout_reason = Some("converged");
21580                    }
21581                    break;
21582                }
21583                if replan_iterations >= MAX_REPLAN {
21584                    if let Some(mut t) = telemetry.take() {
21585                        t.bailout_reason = Some("max_replan");
21586                        t.replan_iterations = replan_iterations;
21587                        self.last_virtual_dep_telemetry = t;
21588                    }
21589                    return Err(
21590                        self.replan_exhausted_error(MAX_REPLAN, "dynamic dependency evaluation")
21591                    );
21592                }
21593                replan_iterations += 1;
21594            }
21595
21596            if let Some(mut t) = telemetry {
21597                t.replan_iterations = replan_iterations;
21598                self.last_virtual_dep_telemetry = t;
21599            }
21600
21601            self.redirty_for_next_recalc();
21602            self.recalc_epoch = self.recalc_epoch.wrapping_add(1);
21603
21604            Ok(EvalResult {
21605                computed_vertices,
21606                cycle_errors,
21607                elapsed: start.elapsed(),
21608            })
21609        })();
21610        log.end_compound();
21611        result
21612    }
21613
21614    /// Evaluate a single layer with ChangeLog recording.
21615    fn evaluate_layer_logged(
21616        &mut self,
21617        layer: &super::scheduler::Layer,
21618        log: &mut ChangeLog,
21619    ) -> Result<usize, ExcelError> {
21620        self.resource_checkpoint(layer.vertices.len() as u64)?;
21621        self.evaluate_layer_units(layer, None, Some(log), None, true)
21622    }
21623}